notebook notebook
首页
  • 计算机网络
  • 计算机系统
  • 数据结构与算法
  • 计算机专业课
  • 设计模式
  • 前端 (opens new window)
  • Java 开发
  • Python 开发
  • Golang 开发
  • Git
  • 软件设计与架构
  • 大数据与分布式系统
  • 常见开发工具

    • Nginx
  • 爬虫
  • Python 数据分析
  • 数据仓库
  • 中间件

    • MySQL
    • Redis
    • Elasticsearch
    • Kafka
  • 深度学习
  • 机器学习
  • 知识图谱
  • 图神经网络
  • 应用安全
  • 渗透测试
  • Linux
  • 云原生
面试
  • 收藏
  • paper 好句
GitHub (opens new window)

学习笔记

啦啦啦,向太阳~
首页
  • 计算机网络
  • 计算机系统
  • 数据结构与算法
  • 计算机专业课
  • 设计模式
  • 前端 (opens new window)
  • Java 开发
  • Python 开发
  • Golang 开发
  • Git
  • 软件设计与架构
  • 大数据与分布式系统
  • 常见开发工具

    • Nginx
  • 爬虫
  • Python 数据分析
  • 数据仓库
  • 中间件

    • MySQL
    • Redis
    • Elasticsearch
    • Kafka
  • 深度学习
  • 机器学习
  • 知识图谱
  • 图神经网络
  • 应用安全
  • 渗透测试
  • Linux
  • 云原生
面试
  • 收藏
  • paper 好句
GitHub (opens new window)
  • 深度学习

    • Posts

    • PyTorch 入门

    • 鱼书进阶-自然语言处理

      • 神经网络的复习
      • 自然语言和单词的分布式表示
      • word2vec
      • word2vec 高速化
      • RNN
      • Gated RNN
      • seq2seq
        • 1. 使用语言模型生成文本
          • 1.1 使用 RNN 生成文本的步骤
          • 1.2 文本生成的实现
          • 1.3 更好的文本生成
        • 2. seq2seq 模型
          • 2.1 seq2seq 的原理
          • 2.2 时序数据转换的简单尝试
          • 2.3 seq2seq 的实现
        • 3. seq2seq 的改进
          • 3.1 反转输入数据(Reverse)
          • 3.2 偷窥(Peeky)
        • 4. seq2seq 的应用
          • 4.1 聊天机器人
          • 4.2 算法学习
          • 4.3 自动图像描述
      • Attention
    • 深度学习-李宏毅

    • 李宏毅-2017版

    • 李宏毅-2019版

    • 预训练语言模型-邵浩2021版

    • 王树森

  • 机器学习

  • 知识图谱

  • AI
  • 深度学习
  • 鱼书进阶-自然语言处理
yubin
2022-04-01
目录

seq2seq

本章我们将利用 LSTM 实现几个有趣的应用。首先将使用语言模型进行文本生成,然后再介绍 seq2seq 的神经网络,将一个时序数据转换为另一个时序数据。

# 1. 使用语言模型生成文本

语言模型可用于各种各样的应用,其中具有代表性的例子有机器翻译、语音识别和文本生成。这里,我们将使用语言模型来生成文本。

# 1.1 使用 RNN 生成文本的步骤

上一章我们用 LSTM 层实现了语言模型,网络结构如下:

image-20220401205251936

现在我们来说明一下语言模型生成文本的顺序。这里仍以“you say goobye and i say hello.”这一在语料库上学习好的语言模型为例,考虑将单词 i 赋给这个语言模型的情况:

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

语言模型计算出下一个出现的单词的概率分布后,它如何生成下一个新单词呢?一种方法是选择概率最高的单词,这时结果能唯一确定;另一种方法是根据概率分布进行选择,这样概率高的单词容易被选到,这时被采样到的单词每次都不一样。

这里我们想让每次生成的文本有所变化,因此我们使用后一种方法来选择单词。下面是生成的过程:

image-20220401212135069

之后根据需要重复此过程即可,直到出现 <eos> 这一结尾记号,这样一来,我们就可以生成新的文本。

这里需要注意的是,像上面这样生成的新文本是训练数据中没有的新生成的文本。因为语言模型并不是背诵了训练数据,而是学习了训练数据中单词的排列模式。如果语言模型通过语料库正确学习了单词的出现模式,我们就可以期待该语言模型生成的文本对人类而言是自然的、有意义的。

# 1.2 文本生成的实现

下面我们进行文本生成的实现,我们基于上一章的 Rnnlm 类来创建继承自它的 RnnlmGen 类,然后向这个类添加生成文本的方法:

class RnnlmGen(Rnnlm):
    def generate(self, start_id, skip_ids=None, sample_size=100):
        word_ids = [start_id]

        x = start_id
        while len(word_ids) < sample_size:
            x = np.array(x).reshape(1, 1)
            score = self.predict(x)
            p = softmax(score.flatten())

            sampled = np.random.choice(len(p), size=1, p=p)
            if (skip_ids is None) or (sampled not in skip_ids):
                x = sampled
                word_ids.append(int(x))

        return word_ids
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

这个类用 generate(start_id, skip_ids, sample_size) 生成本文,此处 start_id 是第一个单词 ID,sample_size 表示要采样的单词数量,skip_ids 是单词 ID 列表,它指定的单词将不被采样,这个参数用于排除 PTB 数据集中的 <unk> 、N 等被预处理过的单词。generate 方法首先通过 model.predict(x) 输出各个单词的得分,然后基于 p=softmax(score) 对得分进行正则化,这样就获得了我们想要的概率分布。

PTB 数据集对原始文本进行了预处理,稀有单词被 <unk> 替换,数字被 N 替换。另外,我们用 <eos> 作为文本的分隔符。

现在,使用这个 RnnlmGen 类进行文本生成,这里先在完全没有学习的状态(即权重参数是随机初始值的状态)下生成文本。我们将第一个单词设为 you,可以得到如下的句子:

image-20220401224157250

可见,输出的文本是一堆乱七八糟的单词,不过这可以理解,因此这里的模型权重使用的是随机初始值,所以输出了没有意义的文本。我们再利用上一章学习好的权重来进行文本生生成,为此使用 model.load_params(...) 读入学习好的参数,并生成文本:

image-20220401224448455

虽然上面的结果中可以看到多处语法错误和意思不通的地方,不过也有几处读起来已经比较像句子了。这个实验生成的文本在某种程度上可以说是正确的,不过结果中仍有许多不自然的地方,改进空间很大。

# 1.3 更好的文本生成

之前我们改进了语音模型,实现了 BetterRnnlm 类们现在我们继承它并用于文本生成,结果如下:

image-20220401224753529

可以看出,这个模型生成了比之前更自然的文本。

# 2. seq2seq 模型

文本数据、音频数据和视频数据都是时序数据,并存在许多需要将一种时序数据转换为另一种时序数据的任务,比如机器翻译、语音识别、聊天机器人、将源代码转为机器语言的编译器等。像这样,世界上存在许多输入输出均为时序数据的任务。现在我们会考察将时序数据转换为其他时序数据的模型,作为实现,我们将介绍使用两个 RNN 的 seq2seq 模型。

# 2.1 seq2seq 的原理

seq2seq 模型也称为 Encoder-Decoder 模型。这个模型有两个模块——Encoder(编码器)和 Decoder(解码器)。编码器对输入数据进行编码,解码器对被编码的数据进行解码。seq2seq 基于编码器和解码器将一个时序数据转换为另一个时序数据。

举一个例子来说明其机制,如下图,seq2seq 将日语翻译成了英语:

image-20220402093558733
  • 编码器编码的信息浓缩了翻译所必需的信息,解码器基于这个浓缩的信息生成目标文本

我们首先看一下编码器的细节:

image-20220402094135432

可以看出,编码器利用 RNN 将时序数据转换为隐藏状态 ,它是一个固定长度的向量。说到底,编码就是将任意长度的文本转换为一个固定长度的向量:

image-20220402094345128

解码器是如何“处理”这个编码好的向量 ,从而生成目标文本的呢?其实我们只需要利用之前讨论的进行文本生成的模型即可:

image-20220402094647101
  • 可以看出解码器的结构和文本生成的神经网络完全相同,不过存在一点点差异,就是 LSTM 层会接收向量 ,而之前的模型不接收任何信息,这个唯一的、微小的改变使得普通的语言模型进化为可以驾驭翻译的解码器。

我们使用了 <eos> 这一分隔符作为通知解码器开始生成文本的信号,另外,解码器采样到 <eos> 为止,作为结束信号。其他文献中也有使用 <go>、<start> 或 _ 作为分隔符。

现在我们连接编码器和解码器,并给出它的层结构:

image-20220402095658527

seq2seq 由两个 LSTM 层构成,即编码器的 LSTM 和解码器的 LSTM,此时 LSTM 层的隐藏状态是编码器和解码器的桥梁:

  • 正向传播时,编码器的编码信息通过 LSTM 层的隐藏状态传递给解码器
  • 反向传播时,解码器的梯度通过这个“桥梁”传递给编码器

# 2.2 时序数据转换的简单尝试

首先说明一下我们要处理的问题。这里我们将“加法”视为一个时序转换问题,效果如图:

image-20220402100347204

这种为了评价机器学习而创建的简单问题,称为 “toy problem”。

seq2seq 对加法逻辑一无所知,它从样本中出现的字符模式,真的可以学习到加法运算的规则吗?这正是本次实验的看头。在之前我们都把文本以单词为单位进行了分割,但并非必须这样做,这次我们将不以单词为单位,而是以字符为单位进行分割,比如“57 + 5”这样的输入会被处理为 ['5', '7', '+', '5'] 这样的列表。

还有一个问题,不同加法的字符数是不同的,比如“57 + 5”共有 4 个字符,而“628 + 521”共有 7 个字符。如此,在加法问题中,每个样本在时间方向上的大小不同。也就是说,加法问题处理的是可变长度的时序数据。

在使用批数据进行学习时,会一起处理多个样本。此时,(在我们的实现中)需要保证一个批次内各个样本的数据形状是一致的。

在基于 mini-batch 学习可变长度的时序数据时,最简单的方法是使用填充(padding)。填充就是用无效(无意义)数据填入原始数据,从而使数据长度对齐:

image-20220402100914788

制作出如下的数据集并使用:

image-20220402101024174

数据集原本应分成训练用、验证用和测试用 3 份。用训练数据进行学习,用验证数据进行调参,最后再用测试数据评价模型的能力。而简单起见,这里只分成训练数据和测试数据 2 份,用它们进行模型的训练和评价。

# 2.3 seq2seq 的实现

seq2seq 是组合了两个 RNN 的神经网络。这里我们首先将这两个 RNN 实现为 Encoder 类和 Decoder 类,然后将这两个类组合起来,来实现 seq2seq 类。

# 2.3.1 Encoder 类

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

image-20220402101230018

它的层结构展开后如下图:

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

编码器只将 LSTM 的隐藏状态传递给解码器。尽管也可以把 LSTM 的记忆单元传递给解码器,但我们通常不太会把 LSTM 的记忆单元传递给其他层。这是因为,LSTM 的记忆单元被设计为只给自身使用。

我们已经将时间方向上进行整体处理的层实现为了 Time LSTM 层和 Time Embedding 层,通过使用他们,我们的编码器将如下图所示:

image-20220402103534054

在初始化时,因为这次并不保持 Time LSTM 层的状态,所以设定 stateful=False。

之前的语言模型处理的是只有一个长时序数据的问题,那时我们设定 Time LSTM 层的参数 stateful=True,以在保持隐藏状态的同时,处理长时序数据。而这次是有多个短时序数据的问题。因此,针对每个问题重设 LSTM 的隐藏状态(为 0 向量)。

简单看一下其 forward 函数:

def forward(self, xs):
	xs = self.embed.forward(xs)
	hs = self.lstm.forward(xs)
	self.hs = hs
	return hs[:, -1, :]
1
2
3
4
5

在编码器的反向传播中,LSTM 层的最后一个隐藏状态的梯度是 dh,这个 dh 是从解码器传来的梯度:

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
1
2
3
4
5
6

# 2.3.2 Decoder 类

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

image-20220402103950452

解码器可以由 RNN 实现,我们这里使用 LSTM:

image-20220402124341110

这里使用了监督数据 _62 进行学习,此时输入数据是 ['_', '6', '2',' '],对应的输出是 ['6', '2', ' ', ' ']。

在使用 RNN 进行文本生成时,学习时和生成时的数据输入方法不同:

  • 在学习时,因为已经知道正确解,所以可以整体地输入时序方向上的数据。
  • 在推理时(生成新字符串时),则只能输入第 1 个通知开始的分隔符(本次为“_”)。然后,基于输出采样 1 个字符,并将这个采样出来的字符作为下一个输入,如此重复该过程。

之前我们进行文本生成时,使用了概率分布来采样,因此生成的文本会随机变动。因为这次的问题是加法,所以我们想消除这种概率性的“波动”,生成“确定性的”答案。为此,这次我们仅选择得分最高的字符。下面是解码器生成字符串的过程:

image-20220402124819905
  • argmax 是获取最大值的索引(本例中是字符 ID)的节点。
  • 这次没有使用 Softmax 层,而是从 Affine 层输出的得分中选择了最大值的字符 ID

如上所述,在解码器中,在学习时和在生成时处理 Softmax 层的方式是不一样的。因此,Softmax with Loss 层交给此后实现的 Seq2seq 类处理。由此画出 Decoder 类的结构:

image-20220402125037813

# 2.3.3 seq2seq 类

最后来看 Seq2seq 类的实现,这里需要做的只是将 Encoder 类和 Decoder 类连接在一起,然后使用 Time Softmax with Loss 层计算损失而已:

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
1
2
3
4
5
6
7
8
9

# 2.3.4 seq2seq 的评价

Seq2seq 的学习和基础神经网络的学习具有相同的流程。基础神经网络的学习流程如下:

  1. 从训练数据中选择一个 mini-batch
  2. 基于 mini-batch 计算梯度
  3. 使用梯度更新权重

这里使用 Trainer 类进行上述操作,另外,seq2seq 针对每个 epoch 求解测试数据(生成字符串),并计算正确率:

model = Seq2seq(vocab_size, wordvec_size, hidden_size)
optimizer = Adam()
trainer = Trainer(model, optimizer)

acc_list = []
for epoch in range(max_epoch):
    trainer.fit(x_train, t_train, max_epoch=1,
                batch_size=batch_size, max_grad=max_grad)

    correct_num = 0
    for i in range(len(x_test)):
        question, correct = x_test[[i]], t_test[[i]]
        verbose = i < 10
        correct_num += eval_seq2seq(model, question, correct,
                                    id_to_char, verbose, is_reverse)

    acc = float(correct_num) / len(x_test)
    acc_list.append(acc)
    print('val acc %.3f%%' % (acc * 100))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  • 这里采 用正确率(正确回答了多少问题)作为评价指标

运行后的结果为:

image-20220402130811413
  • 每个 epoch 显示一次结果,每个问题中, Q 代表问题,T 代表正确答案,第三行代表模型给出的答案。

随着学习的进行,上面的结果会发生什么样的变化:

image-20220402130950215

从结果中可知,seq2seq 最开始没能顺利回答问题。但是,随着学习不断进行,它在慢慢靠近正确答案,然后变得可以正确回答一些问题,直到最后的正确率约为 10%:

image-20220402192455313

为了能更好地学习相同的问题,我们对 seq2seq 做一些改进。

# 3. seq2seq 的改进

我们对之前的 seq2seq 进行改进,以改进学习的进展。

# 3.1 反转输入数据(Reverse)

第一个改进方案是非常简单的技巧——反转输入数据的顺序:

image-20220402192216350

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

为了反转输入数据,在读入数据集之后,我们追加下面的代码:



 

(x_train, t_train), (x_test, t_test) = sequence.load_data('addition.txt')
...
x_train, x_test = x_train[:, ::-1], x_test[:, ::-1]
1
2
3

通过反转输入数据,正确率可以上升多少呢:

image-20220402194731266

仅仅通过反转输入数据,学习的进展就得到了极大改善!虽然反转数据的效果因任务而异,但是通常都会有好的结果。

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

# 3.2 偷窥(Peeky)

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

image-20220402200304363

为了达成该目标,seq2seq 的第二个改进方案就应运而生了——将这个集中了重要信息的编码器的输出 分配给解码器的其他层。改进后的解码器如图:

image-20220402200411443
  • 将编码器的输出 分配给所有时刻的 LSTM 层和 Affine 层

相较于之前,LSTM 层专用的重要信息 现在在多个层中共享了。

这里的改进是将编码好的信息分配给解码器的其他层,这可以解释为其他层也能“偷窥”到编码信息。因为“偷窥”的英语是 peek,所以将这个改进了的解码器称为 Peeky Decoder,其相应的 seq2seq 称为 Peeky seq2seq。

上图的结构中,有两个向量同时被输入到了 LSTM 层和 Affine 层,这实际上表示两个向量的拼接(concatenate)。如果使用 concat 节点拼接两个向量,则正确的计算图可以绘制成下图形式:

image-20220402203848285

编码器与上一节没有变化。由于其实现并不难,我们省略了 PeekyDecoder 类的实现。

最后我们看一下实现 PeekySeq2seq,这和上一节的 Seq2seq 类基本相同,唯一的区别是 Decoder 层:

class PeekySeq2seq(Seq2seq):
    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 = PeekyDecoder(V, D, H)
        self.softmax = TimeSoftmaxWithLoss()

        self.params = self.encoder.params + self.decoder.params
        self.grads = self.encoder.grads + self.decoder.grads
1
2
3
4
5
6
7
8
9

至此,准备工作就完成了,现在我们使用这个 PeekySeq2seq 类,再次挑战加法问题:

model = PeekySeq2seq(vocab_size, wordvec_size, hidden_size)
1

可以看到结果如图所示:

image-20220402205923166

加上了 Peeky 的 seq2seq 的结果大幅变好。刚过 10 个 epoch 时,正确率已经超过 90%,最终的正确率接近 100%。

从上述实验结果可知,Reverse 和 Peeky 都有很好的效果。借助反转输入语句的 Reverse 和共享编码器信息的 Peeky,我们获得了令人满意的结果!

这里的实验有几个需要注意的地方。因为使用 Peeky 后,网络的权重参数会额外地增加,计算量也会增加,所以这里的实验结果必须考虑到相应地增加的“负担”。另外,seq2seq 的精度会随着超参数的调整而大幅变化。虽然这里的结果是可靠的,但是在实际问题中,它的效果可能不稳定。

# 4. seq2seq 的应用

seq2seq 将某个时序数据转换为另一个时序数据,这个转换时序数据的框架可以应用在各种各样的任务中:

  • 机器翻译:将“一种语言的文本”转换为“另一种语言的文本”
  • 自动摘要:将“一个长文本”转换为“短摘要”
  • 问答系统:将“问题”转换为“答案”
  • 邮件自动回复:将“接收到的邮件文本”转换为“回复文本”

像这样,seq2seq 可以用于处理成对的时序数据的问题。除了自然语言之外,也可以用于语音、视频等数据。

# 4.1 聊天机器人

聊天机器人是人和计算机使用文本进行对话的程序,因为对话是由“对方的发言”和“本方的发言”构成的,可以理解为是将“对方的发言”转换为“本方的发言”的问题。也就是说,如果有对话文本数据,seq2seq 就可以学习它:

image-20220402210807013

这个机器人还无法泛化,但是,基于对话获取答案或者线索,这一点非常实用,应用范围很广。

# 4.2 算法学习

本章进行的 seq2seq 实验是加法这样的简单问题,但理论上它也能处理更加高级的问题:

image-20220402210929509

可以直接将源代码输入 seq2seq,让 seq2seq 对源代码与目标答案一起进行学习。不过这类问题并不太好解决,通过改造 seq2seq 的结构,可以期待这样的问题能够被解决。

# 4.3 自动图像描述

自动图像描述将“图像”转换为“文本”:

image-20220402211121286

这时我们熟悉的网络结构,实际上,它和之前的网络的唯一区别在于,编码器从 LSTM 换成了 CNN,而解码器仍使用与之前相同的网络。仅通过这点改变(用 CNN 替代 LSTM),seq2seq 就可以处理图像了。

现在我们看几个基于 seq2seq 的自动图像描述的例子,这时由基于 TensorFlow 的 im2txt 生成的例子:

image-20220402211239229

由图可知,这里得到了很不错的结果。之所以能够达到这样的效果,是因为存在大量的图像和说明文字等训练数据,再加上可以高效学习这些训练数据的 seq2seq 的应用,最终得到了如图所示的出色结果。

下一章我们将继续改进 seq2seq,届时深度学习中最重要的技巧之一 Attention 将会出现。

编辑 (opens new window)
上次更新: 2022/08/26, 13:48:42
Gated RNN
Attention

← Gated RNN Attention→

最近更新
01
Deep Reinforcement Learning
10-03
02
误删数据后怎么办
04-06
03
MySQL 一主多从
03-22
更多文章>
Theme by Vdoing | Copyright © 2021-2024 yubincloud | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×