利用神经网络学习语言(四)——深度循环神经网络

相关说明

这篇文章的大部分内容参考自我的新书《解构大语言模型:从线性回归到通用人工智能》,欢迎有兴趣的读者多多支持。
本文涉及到的代码链接如下:regression2chatgpt/ch10_rnn/char_rnn_batch.ipynb

《循环神经网络(RNN)》这篇文章讨论了RNN的模型结构,并展示了如何利用该模型来学习语言。然而,文中的代码实现只是示范性的,运行效率不高,限制了模型在实际应用中的效果。本文将重点讨论如何高效地实现循环神经网络,并在此基础上构建深度循环神经网络(Deep RNN)。

在理解本文的基础上,我们将能够更好地理解著名的长短期记忆网络(LSTM)模型,具体内容请参考:

  • 利用神经网络学习语言(五)——长短期记忆网络(LSTM)

内容大纲

  • 相关说明
  • 一、循环神经网络更优雅的代码实现
  • 二、批量序列数据的处理
  • 三、从单层走向更复杂的结构
  • 四、利用深度循环神经网络学习语言

一、循环神经网络更优雅的代码实现

利用神经网络学习语言(三)——循环神经网络(RNN)中实现了一个最简单的单层循环神经网络,本文将进一步讨论如何构建更复杂的循环神经网络,并将其应用于自然语言处理。搭建复杂模型的关键在于高效且优雅的代码实现,这也是接下来将要讨论的内容。

模型训练的基础是梯度下降法及其变种(如果不太了解请参考其他的文献[TODO])。在这类算法的每个迭代周期内,首先选择一批数据,然后计算模型在这批数据上的损失并进行反向传播。在这个过程中,不同数据的计算是相互独立的,可以并行执行,这一点同样适用于循环神经网络。同一序列的模型计算必须串行进行,不同序列的计算仍然可以并行处理。例如,同一文本的文字必须按顺序处理,但是不同的文本可以同时并行处理。因此,提高循环神经网络计算效率的关键在于并行处理批量数据的模型计算。具体的代码实现如程序清单1所示(完整代码),以下是实现的要点。

  1. 实现并行计算的首要步骤是将批量的数据整合成一个新的更高维度的张量。假设模型的输入数据形状为(B,T,C)1,如第10—13行所示。其中,B代表批量大小(Batch Size),T代表序列数据的长度,C代表每个元素的特征长度。在自然语言处理领域,C通常表示文本嵌入特征的长度。需要注意的是,上述计算中假设所有输入序列具有相同的长度,然而在实际应用中,序列长度不同的情况时有发生。本文的第二部分将介绍如何处理这一细节,以确保模型能够灵活适应不同长度的输入序列。
  2. 根据前面的讨论,模型将为序列数据中的每个元素生成一个对应的隐藏状态。因此,模型的输出形状将为(B,T,H),如第22行和第23行代码所示。其中,H表示隐藏状态的长度。
  3. 因为序列数据内部需要进行循环处理,所以需要对输入张量进行转置,将其形状变为(T,B,C),详见第14行。然后,将之前在模型外部执行的循环步骤移到模型内部。具体来说,将这篇文章中的程序清单2中的第11行和第12行稍做修改,变成程序清单1中的第17—20行。
程序清单1 批量循环神经网络
 1 |  class RNN(nn.Module):2 |  3 |      def __init__(self, input_size, hidden_size):4 |          super().__init__()5 |          self.hidden_size = hidden_size6 |          self.i2h = nn.Linear(input_size + hidden_size, hidden_size)7 |  8 |      def forward(self, x, hidden=None):9 |          re = []
10 |          # B batch_size,
11 |          # T sequence length,
12 |          # C number of channels.
13 |          B, T, C = x.shape
14 |          x = x.transpose(0, 1) # (T, B, C)
15 |          if hidden is None:
16 |              hidden = self.init_hidden(B, x.device)
17 |          for i in range(T):
18 |              # x[i]: (B, C); hidden: (B, H)
19 |              combined = torch.cat((x[i], hidden), dim=1)
20 |              hidden = F.relu(self.i2h(combined))  # (   B, H)
21 |              re.append(hidden)
22 |          result_tensor = torch.stack(re, dim=0)   # (T, B, H)
23 |          return result_tensor.transpose(0, 1)     # (B, T, H)
24 |  
25 |      def init_hidden(self, B, device):
26 |          return torch.zeros((B, self.hidden_size), device=device)

二、批量序列数据的处理

上述模型实现对输入数据提出了一项相对严格的要求,即输入序列的长度必须相同。为了使这个模型更具通用性,能够适应长度不一的批量序列数据,通常有两种常见的处理方法。

一种是填充数据,即根据批量数据中最长的序列长度,使用特殊字符来填充其他序列的空白部分。这种处理方式存在一些问题。首先,由于需要使用特殊字符进行填充,会导致不必要的计算开销(填充字符并不需要进行模型计算)。其次,填充的特殊字符可能使模型产生误解,因为模型无法区分哪部分数据是不需要学习的。为了应对上述两个问题,PyTorch提供了一系列封装函数,用于更高效地处理填充数据和模型计算,其中包括pack_padded_sequence和pad_packed_sequence等函数。关于这些函数的详细信息,请读者查阅官方文档。鉴于篇幅有限,本文不再赘述。

另一种是截断。截断意味着设定一个文本长度T,然后按照这个长度从文本中截取片段用于训练。

在大语言模型中,更常用的方法是截断而不是填充。这样选择有两个主要原因。首先,虽然循环神经网络能够处理任意长度的序列数据,但当处理长距离依赖时,它会面临挑战。因此,太长的序列数据对模型的优化帮助不大,选择一个合适的序列长度更有益。其次,采用截断方法可以非常方便地多次学习同一文本。具体来说,如果文本的总长度为 L L L,那么可以逐步滑动窗口,生成 L − T + 1 L-T+1 LT+1个序列片段(相互重叠)。这种方式大幅扩充了训练数据的数量,有助于提高模型的泛化能力。

图1所示为截断方法的一种实现,下面将使用它来准备训练数据。一些细心的读者可能会产生疑虑:这个实现似乎不能很好地处理文本长度小于 T T T的情况。的确如此,图1中提供的实现存在一些不足之处,无法处理这种“特殊”情况。它只是一个比较直观的示范,而非最佳实践。

图1

图1

在实际应用中,为了更全面地处理各种情况,通常采取以下方法进行截断:在每个文本的末尾添加一个特殊字符来表示结束,比如之前提到的“<|e|>”。然后,将所有文本连接成一个长串,截断操作将在这个长串上进行。这种方式能够保证即使某个文本的长度小于 T T T,也能正常处理,只是这种情况下截取的数据中包含了表示结束的特殊字符。

三、从单层走向更复杂的结构

前文实现的循环神经网络是单层的网络结构。在横向上,模型可以根据输入的序列数据自动展开成一个宽度很大的结构,但从纵向上看,神经网络仍然只有一层。为了提升模型的表达能力,类似于多层感知器,循环神经网络也可以增加层数。这意味着可以通过叠加多个循环神经网络层来构建深度循环神经网络(Deep RNN)2,如图2所示。深度循环神经网络的展开形式是一个宽度和高度都很大的网络结构。从工程的角度来看,模型会首先横向传递信号,然后逐层向上,一层一层地计算。这种方法可以最大限度地实现并行处理,提高模型的效率。

图2

图2

前文所讨论的模型不仅是单层的,而且是单向的,即单向循环神经网络(Unidirectional RNN)——模型按照顺序从左到右处理序列数据。这种处理方式的假设是当前数据只依赖左侧文字的背景。然而,在实际应用中,这种假设并不总是成立。以英语为例,冠词“a”和“an”的使用取决于右侧文字,比如“a boy”和“an example”。为了提升模型效果,需要引入双向循环神经网络(Bidirectional RNN),如图3所示。这种双向设计使得模型能够更全面地捕捉序列中的依赖关系和模式,提高模型对复杂序列数据的建模能力。在自然语言处理领域,自回归模式只需要单向循环神经网络。但在自编码模式下,要预测被遮蔽的内容,需要考虑左右两侧的上下文,通常需要双向循环神经网络。

值得注意的是,图3所示的双向循环神经网络仍然只是单层神经结构,也可以叠加多层来提升模型效果。此外,双向循环神经网络的输出使用了张量拼接的处理技巧。这种简单且直观的处理方法在后续的许多复杂神经网络中也经常出现。

图3

图3

按照输入/输出的张量形状进行分类,前面讨论的循环神经网络都属于不定长输入、不定长输出的模型,对应这篇文章图1中的标记3。这种模型被称为标准的循环神经网络,具有广泛的适用性,简单调整后可适应各种复杂的应用场景。下面以标准循环神经网络为基础举例,构建一个用于解决翻译问题的模型,其具体结构如图4所示。

图4

图4

模型中包含两个标准循环神经网络,分别被称为编码器(Encoder)和解码器(Decoder)3。在图4中,编码器会逐步读入中文文本。它为每个输入的词元生成相应的隐藏状态,我们主要关注最后一个隐藏状态,它包含了整个文本的信息,会被传递给解码器。

解码器的运作方式与编码器有所不同,它不需要生成全零的初始隐藏状态,而是接收来自编码器的隐藏状态作为起点。在解码器中,初始的输入序列是表示文本开始的特殊字符“<|b|>”。与文本生成时的步骤类似,解码器会将预测结果添加到输入序列中,然后再次触发模型预测(这个过程依赖其他模型组件,例如图4中的语言建模头lmh)。如此循环,解码器逐步生成目标文本,完成翻译任务。

按照输入/输出的张量形状进行分类,编码器是不定长输入、定长输出的模型,对应这篇文章图1中的标记2。解码器将定长输入映射到不定长输出,对应这篇文章图1中的标记4。当将编码器和解码器结合在一起时,就构成了这篇文章图1中的标记5。

四、利用深度循环神经网络学习语言

根据上述讨论,下面将构建一个双层的单向循环神经网络,用于语言学习。具体的模型细节可以参考程序清单2。为了防止过拟合,模型对每一层的输出进行了随机失活(Dropout)操作,如第16—17行代码所示。在实际应用中,这一设计几乎已经成为循环神经网络的标准配置。

程序清单2 深度循环神经网络
 1 |  class CharRNNBatch(nn.Module):2 |  3 |      def __init__(self, vs):4 |          super().__init__()5 |          self.emb_size = 2566 |          self.hidden_size = 1287 |          self.embedding = nn.Embedding(vs, self.emb_size)8 |          self.dp = nn.Dropout(0.4)9 |          self.rnn1 = RNN(self.emb_size, self.hidden_size)
10 |          self.rnn2 = RNN(self.hidden_size, self.hidden_size)
11 |          self.h2o = nn.Linear(self.hidden_size, vs)
12 |  
13 |      def forward(self, x):
14 |          # x: (B, T)
15 |          emb = self.embedding(x)      # (B, T,  C)
16 |          h = self.dp(self.rnn1(emb))  # (B, T,  H)
17 |          h = self.dp(self.rnn2(h))    # (B, T,  H)
18 |          output = self.h2o(h)         # (B, T, vs)
19 |          return output

与这篇文章的结果相比,新模型的预测效果有了一定改善,但仍然未能达到理想水平。如图5所示,它的表现与多层感知器旗鼓相当,这主要是由于循环神经网络在处理长距离依赖关系时表现不佳,本系列的后续文章将深入讨论这一问题。

图5

图5

值得注意的是,如果参考图2,本节使用随机失活的方式实际上是对神经网络中的纵向信号进行处理。然而,这不是唯一的方式,也可以对神经网络中的横向信号执行相似的操作,也就是在隐藏状态上引入失活操作。这一方法在学术界被称为循环失活(Recurrent Dropout)。

虽然PyTorch提供的封装中并不包含循环失活,但实现这一功能并不困难,只需在程序清单1的基础上稍做修改即可(鉴于篇幅,具体修改细节不再详述)。这个例子清楚展示了神经网络的灵活性,同时印证了我们不仅需要理解神经网络模型设计的细节,还需要通过代码实现它。这样就可以根据实际需求自主增添相应的模型组件,而不必受限于开源工具的开发进度和设计选择。


  1. 本文提供的实现与PyTorch中的nn.RNN相比,虽然核心实现类似,但是具体的接口封装上有一些不同之处。在输入数据方面,本节要求数据的第一个维度表示批量大小,而在PyTorch的封装中,默认情况却不是这样的。此外,PyTorch的封装还支持双向和多层的循环神经网络,而本节的实现是单层单向的。 ↩︎

  2. 这种模型也可以被称为多层循环神经网络(Multi-layer RNN)。 ↩︎

  3. 著名的语言模型Transformer,其基本结构也分为编码器和解码器两个主要部分。 ↩︎

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/14346.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【移花接木】OpenCV4.8 For Java 深度学习 实时人脸检测

学习《OpenCV应用开发&#xff1a;入门、进阶与工程化实践》一书&#xff0c;学会本文所有技能就这么简单&#xff01; 做真正的OpenCV开发者&#xff0c;从入门到入职&#xff0c;一步到位&#xff01; 前言 我写这篇文章之前&#xff0c;我搜索整个网络文章跟问各种语言大模…

速卖通测评揭秘:如何选择安全的渠道操作

许多商家对测评存在误解&#xff0c;认为只需进行几次测评就能迅速打造爆款。实际上&#xff0c;测评是一个需要计划和持久性的过程&#xff0c;以便让平台检测到产品的受众程度并提高产品的曝光和权重。 在进行测评时&#xff0c;安全是首要考虑的问题。平台可以通过设备、网…

黑马点评1——短信篇(基于session)

&#x1f308;hello&#xff0c;你好鸭&#xff0c;我是Ethan&#xff0c;一名不断学习的码农&#xff0c;很高兴你能来阅读。 ✔️目前博客主要更新Java系列、项目案例、计算机必学四件套等。 &#x1f3c3;人生之义&#xff0c;在于追求&#xff0c;不在成败&#xff0c;勤通…

如何使用多种算法解决LeetCode第135题——分发糖果问题

❤️❤️❤️ 欢迎来到我的博客。希望您能在这里找到既有价值又有趣的内容&#xff0c;和我一起探索、学习和成长。欢迎评论区畅所欲言、享受知识的乐趣&#xff01; 推荐&#xff1a;数据分析螺丝钉的首页 格物致知 终身学习 期待您的关注 导航&#xff1a; LeetCode解锁100…

Ubuntu中 petalinux 安装 移植linux --tftp/tftp-hpa服务的方法

Xilinx 文档 PetaLinux 指南&#xff1a;如何创建 PetaLinux 环境 &#xff08;2019.1&#xff09; PetaLinux工具参考指南 PetaLinux安装详解(Xilinx , linux, zynq, zynqMP) petalinux 2020.1安装教程 一、PetaLinux工具和库安装 PetaLinux 工具要求主机系统 /bin/sh 为“b…

笔记 | 《css权威指南》

网络安全色 URL text-indent line-height & vertical-align 字体 font-weight 400 normal 700 bold background-attachment

【调试笔记-20240516-Windows-使用VS2019编译edk2(上)】

调试笔记-系列文章目录 调试笔记-20240516-Windows-使用VS2019编译edk2&#xff08;上&#xff09; 文章目录 调试笔记-系列文章目录调试笔记-20240516-Windows-使用VS2019编译edk2&#xff08;上&#xff09; 前言一、安装开发工具1. 安装 VS20192. 安装 Python 3.103. 安装 …

pdf加水印怎么加?3种添加水印方法分享

pdf加水印怎么加&#xff1f;PDF加水印不仅是为了保护文档内容&#xff0c;确保信息的安全性和完整性&#xff0c;更是一种有效的版权保护措施。通过添加水印&#xff0c;您可以在文档中嵌入公司名称、日期、编号等信息&#xff0c;以明确文档的归属权和使用限制。此外&#xf…

小而美:两步完成从源码到应用的极简交付

作者&#xff1a;花三&#xff08;王俊&#xff09; Serverless 应用引擎 SAE 是阿里云推出的一款零代码改造、极简易用、自适应弹性的容器化应用托管平台&#xff0c;面市以来为几万家企业客户提供服务&#xff0c;运行稳定&#xff0c;广受好评。 SAE 的出现解决了众多企业…

运行时异常和编译时异常的区别

Java中的异常被分为两大类&#xff1a;编译时异常和运行时异常。 都是RuntimeException类及其子类异常&#xff0c;如NullPointerException、IndexOutOfBoundsException。这些异常是不检查异常&#xff0c;运行时异常的特点是Java编译器不会检查它&#xff0c;程序中可以选择捕…

纯代码如何实现WordPress搜索包含评论内容?

WordPress自带的搜索默认情况下是不包含评论内容的&#xff0c;不过有些WordPress网站评论内容比较多&#xff0c;而且也比较有用&#xff0c;所以想要让用户在搜索时也能够同时搜索到评论内容&#xff0c;那么应该怎么做呢&#xff1f; 网络上很多教程都是推荐安装SearchWP插…

Spring Web MVC介绍及详细教程

目录 1.什么是Spring Web MVC&#xff1f; 1.1 MVC定义 1.2 Spring MVC与MVC关系 2.为什么要学习Spring MVC 3.项目创建 4.Spring MVC连接 4.1 RequestMapping 4.2 PostMapping和GetMapping 5.Spring MVC参数获取 5.1 获取单个参数 5.2 获取多个参数 5.3 获取普通对…

通用代码生成器应用场景一,项目前期

通用代码生成器是一种自动化编程软件&#xff0c;是一种先进的编译系统。它具有表级抽象。把系统抽象为域对象&#xff0c;枚举&#xff0c;弹性登录模块&#xff0c;复杂版面和图形报表。使用通用代码生成器完成项目前期&#xff0c;比直接使用对应的高级语言快的多&#xff0…

element Notification 消息过多需要折叠

Notification 消息过多太长 希望能折叠 如图下效果 element-plus 可以将dom 插入到具体的元素 结合css :nth-child 来控制样式达到效果 element dom 只能插入到body中 所以无法使用:nth-child 1.Notification需要消息提示时设置class let eleNum 0 // 弹窗的序号 function…

vue+canvas实现逐字手写效果

在pc端进行逐字手写的功能。用户可以在一个 inputCanvas 上书写单个字&#xff0c;然后在特定时间后将这个字添加到 outputCanvas 上&#xff0c;形成一个逐字的手写效果。用户还可以保存整幅图像或者撤销上一个添加的字。 <template><div class"container"…

小红书-社区搜索部 (NLP、CV算法实习生) 一面面经

&#x1f604; 整个流程按如下问题展开&#xff0c;用时60min左右面试官人挺好&#xff0c;前半部分问问题&#xff0c;后半部分coding一道题。 各位有什么问题可以直接评论区留言&#xff0c;24小时内必回信息&#xff0c;放心~ 文章目录 1、自我介绍2、介绍下项目&#xff…

淄博公司商标驳回复审条件及流程

商标是人工审查的&#xff0c;所以不同的人会有不同的想法和意见&#xff0c;导致同一案件的审查结果不同。特别是商标审查周期缩短到5个月&#xff0c;全国平均每个工作日有1万多个商标提交申请&#xff0c;而全国只有一个商标审查单位——国家商标局提交申请。这种情况下&…

Java版工程行业管理系统-提升工程项目的综合管理能力

工程项目管理涉及众多环节和角色&#xff0c;如何实现高效协同和信息共享是关键。本文将介绍一个采用先进技术框架的Java版工程项目管理系统&#xff0c;该系统支持前后端分离&#xff0c;功能全面&#xff0c;可满足不同角色的需求。从项目进度图表到施工地图&#xff0c;再到…

C++_vector操作使用

文章目录 &#x1f680;1.1 vector介绍&#x1f680;1.2 vector的初始化&#x1f680;1.3 vector的常用内置函数&#x1f680;1.4 vector的遍历 &#x1f680;1.1 vector介绍 vector是表示可变大小数组的序列容器。就像数组一样&#xff0c;vector也采用的连续存储空间来存储元…

MySQL主从复制(docker搭建)

文章目录 1.MySQL主从复制配置1.主服务器配置1.拉取mysql5.7的镜像2.启动一个主mysql&#xff0c;进行端口映射和目录挂载3.进入/mysql5.7/mysql-master/conf中创建my.cnf并写入主mysql配置1.进入目录2.执行命令写入配置 4.重启mysql容器&#xff0c;使配置生效5.进入主mysql&a…