《深度学习进阶:自然语言处理(第7章)》-读书笔记

第7章 基于RNN生成文本

7.1 使用语言模型生成文本

语言模型根据已经出现的单词输出下一个出现的单词的概率分布。

一种可能的方法是选择概率最高的单词。在这种情况下,因为选择的是概率最高的单词,所以结果能唯一确定。也就是说,这是一种“确定性的”方法。

另一种方法是“概率性地”进行选择。根据概率分布进行选择,这样概率高的单词容易被选到,概率低的单词难以被选到。在这种情况下,被选到的单词(被采样到的单词)每次都不一样。

7.2 seq2seq模型

  • seq2seq的原理

seq2seq 模型也称为 Encoder-Decoder 模型。顾名思义,这个模型有两个模块——Encoder(编码器)和 Decoder(解码器)。编码器对输入数据进行编码,解码器对被编码的数据进行解码。

编码器和解码器协作,将一个时序数据转换为另一个时序数据。另外,在这些编码器和解码器内部可以使用 RNN。

编码器的层结构

上图,编码器利用 RNN(LSTM)将时序数据转换为隐藏状态 h,是 LSTM 层的最后一个隐藏状态,其中编码了翻译输入文本所需的信息。说到底,编码就是将任意长度的文本转换为一个固定长度的向量。

解码器的层结构

上图,解码器的结构和 LSTM 完全相同。不过存在一点差异,就是 LSTM 层会接收向量 h。在之前的语言模型中,LSTM 层不接收任何信息(硬要说的话,也可以说LSTM 的隐藏状态接收“0 向量”)。这个唯一的、微小的改变使得普通的语言模型进化为可以驾驭翻译的解码器。

seq2seq的整体的层结构

上图,seq2seq 由两个 LSTM 层构成,即编码器的 LSTM 和解码器的LSTM。此时,LSTM层的隐藏状态是编码器和解码器的“桥梁”。在正向传播时,编码器的编码信息通过 LSTM 层的隐藏状态传递给解码器;在反向传播时,解码器的梯度通过这个“桥梁”传递给编码器。

7.3 seq2seq的实现

  • Encoder类

Encoder 类接收字符串,将其转化为向量 h。

编码器的层结构

上图,Encoder 类由 Embedding 层和 LSTM 层组成。Embedding 层将字符(字符 ID)转化为字符向量,然后将字符向量输入 LSTM 层。LSTM 层向右(时间方向)输出隐藏状态和记忆单元,向上输出隐藏状态。这里,因为上方不存在层,所以丢弃 LSTM 层向上的输出。在编码器处理完最后一个字符后,输出 LSTM 层的隐藏状态 h。然后,这个隐藏状态 h 被传递给解码器。

class Encoder:
    def __init__(self, vocab_size, wordvec_size, hidden_size):
        """词汇量、字符向量维数、LSTM隐藏状态维数"""
        V, D, H = vocab_size, wordvec_size, hidden_size
        rn = np.random.randn
        embed_W = (rn(V, D) / 100).astype('f')
        lstm_Wx = (rn(D, 4 * H) / np.sqrt(D)).astype('f')
        lstm_Wh = (rn(H, 4 * H) / np.sqrt(H)).astype('f')
        lstm_b = np.zeros(4 * H).astype('f')
        self.embed = TimeEmbedding(embed_W)
        self.lstm = TimeLSTM(lstm_Wx, lstm_Wh, lstm_b, stateful=False)
        self.params = self.embed.params + self.lstm.params
        self.grads = self.embed.grads + self.lstm.grads
        self.hs = None
    def forward(self, xs):
        xs = self.embed.forward(xs)
        hs = self.lstm.forward(xs)
        self.hs = hs
        return hs[:, -1, :]
    def backward(self, dh):
        dhs = np.zeros_like(self.hs)
        dhs[:, -1, :] = dh

        dout = self.lstm.backward(dhs)
        dout = self.embed.backward(dout)
        return dout
  • Decoder类

Decoder 类接收 Encoder 类输出的 h,输出目标字符串。

解码器的层结构(学习时)

上图,解码器在学习时的层结构。这里使用了监督数据 _62 进行学习,此时输入数据是 [‘_’, ‘6’, ‘2’, ’ '],对应的输出是 [‘6’, ‘2’, ’ ', ’ ']。

解码器生成字符串的步骤:通过argmax节点从Affine层的输出中选择最大值的索引(字符ID)

上图,解码器生成字符串的过程。

class Decoder:
    def __init__(self, vocab_size, wordvec_size, hidden_size):
        """词汇量、字符向量维数、LSTM隐藏状态维数"""
        V, D, H = vocab_size, wordvec_size, hidden_size
        rn = np.random.randn
        embed_W = (rn(V, D) / 100).astype('f')
        lstm_Wx = (rn(D, 4 * H) / np.sqrt(D)).astype('f')
        lstm_Wh = (rn(H, 4 * H) / np.sqrt(H)).astype('f')
        lstm_b = np.zeros(4 * H).astype('f')
        affine_W = (rn(H, V) / np.sqrt(H)).astype('f')
        affine_b = np.zeros(V).astype('f')
        self.embed = TimeEmbedding(embed_W)
        self.lstm = TimeLSTM(lstm_Wx, lstm_Wh, lstm_b, stateful=True)
        self.affine = TimeAffine(affine_W, affine_b)
        self.params, self.grads = [], []
        for layer in (self.embed, self.lstm, self.affine):
            self.params += layer.params
            self.grads += layer.grads
    def forward(self, xs, h):
        """学习时使用"""
        self.lstm.set_state(h)
        out = self.embed.forward(xs)
        out = self.lstm.forward(out)
        score = self.affine.forward(out)
        return score
    def backward(self, dscore):
        dout = self.affine.backward(dscore)
        dout = self.lstm.backward(dout)
        dout = self.embed.backward(dout)
        dh = self.lstm.dh
        return dh
    def generate(self, h, start_id, sample_size):
        """生成时使用
           从编码器接收的隐藏状态 h、最开始输入的字符ID start_id 、生成的字符数量 sample_size
        """
        sampled = []
        sample_id = start_id
        self.lstm.set_state(h)
        for _ in range(sample_size):
            x = np.array(sample_id).reshape((1, 1))
            out = self.embed.forward(x)
            out = self.lstm.forward(out)
            score = self.affine.forward(out)
            sample_id = np.argmax(score.flatten())
            sampled.append(int(sample_id))
        return sampled
  • Seq2seq类
class Seq2seq(BaseModel):
    def __init__(self, vocab_size, wordvec_size, hidden_size):
        V, D, H = vocab_size, wordvec_size, hidden_size
        self.encoder = Encoder(V, D, H)
        self.decoder = Decoder(V, D, H)
        self.softmax = TimeSoftmaxWithLoss()
        self.params = self.encoder.params + self.decoder.params
        self.grads = self.encoder.grads + self.decoder.grads
    def forward(self, xs, ts):
        decoder_xs, decoder_ts = ts[:, :-1], ts[:, 1:]
        h = self.encoder.forward(xs)
        score = self.decoder.forward(decoder_xs, h)
        loss = self.softmax.forward(score, decoder_ts)
        return loss
    def backward(self, dout=1):
        dout = self.softmax.backward(dout)
        dh = self.decoder.backward(dout)
        dout = self.encoder.backward(dh)
        return dout
    def generate(self, xs, start_id, sample_size):
        h = self.encoder.forward(xs)
        sampled = self.decoder.generate(h, start_id, sample_size)
        return sampled

这里需要做的只是将 Encoder 类和 Decoder 类连接在一起,然后使用 Time Softmax with Loss 层计算损失。

7.4 seq2seq的改进

  • 反转输入数据(Reverse)

在许多情况下,使用这个技巧后,学习进展得更快,最终的精度也有提高。

# 读入数据集
(x_train, t_train), (x_test, t_test) = sequence.load_data('addition.txt')
...
x_train, x_test = x_train[:, ::-1], x_test[:, ::-1]
...

为什么反转数据后,学习进展变快,精度提高了呢?

虽然理论上不是很清楚,但是直观上可以认为,反转数据后梯度的传播可以更平滑。比如,考虑将“我是一只猫”翻译成“I am a cat”这一问题,单词“我”和单词“I”之间有转换关系。此时,从“我”到“I”的路程必须经过“是”“一”“只”“猫”这 3 个单词的 LSTM 层。因此,在反向传播时,梯度从“I”抵达“我”,也要受到这个距离的影响。如果反转输入语句,也就是变为“猫只一是我”,结果会怎样呢?此时,“我”和“I”彼此相邻,梯度可以直接传递。如此,因为通过反转,输入语句的开始部分和对应的转换后的单词之间的距离变近(这样的情况变多),所以梯度的传播变得更容易,学习效率也更高。不过,在反转输入数据后,单词之间的“平均”距离并不会发生改变。

  • 偷窥(Peeky)

编码器将输入语句转换为固定长度的向量 h,这个 h 集中了解码器所需的全部信息。也就是说,它是解码器唯一的信息源。但是,当前的 seq2seq 只有最开始时刻的 LSTM 层利用了 h。我们能更加充分地利用这个 h 吗?

改进前:只有最开始的LSTM层接收编码器的输出h

上图,将编码器的输出 h 分配给所有时刻的 Affine 层和 LSTM 层。之前 LSTM 层专用的重要信息 h 现在在多个层(在这个例子中有 8 个层)中共享了。重要的信息不是一个人专有,而是多人共享,这样我们或许可以做出更加正确的判断。有两个向量同时被输入到了 LSTM 层和 Affine 层,这实际上表示两个向量的拼接(concatenate)。

reverse + peeky是进行了本节的两个改进的结果

继续阅读:
《深度学习进阶:自然语言处理(第1章)》-读书笔记
《深度学习进阶:自然语言处理(第2章)》-读书笔记
《深度学习进阶:自然语言处理(第3章)》-读书笔记
《深度学习进阶:自然语言处理(第4章)》-读书笔记
《深度学习进阶:自然语言处理(第5章)》-读书笔记
《深度学习进阶:自然语言处理(第6章)》-读书笔记
《深度学习进阶:自然语言处理(第7章)》-读书笔记
《深度学习进阶:自然语言处理(第8章)》-读书笔记

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值