AI Evolution

4.3 序列数据的处理

序列数据(如文本和语音)的处理对模型提出了理解顺序、上下文和长距离依赖的挑战。循环神经网络(RNN)通过引入“记忆”机制初步解决了这一问题,但受限于梯度消失等缺陷。长短期记忆网络(LSTM)通过精巧的门控机制大大增强了长时记忆能力。然而,RNN 架构固有的串行计算瓶颈催生了更高效的注意力机制,它允许模型动态聚焦于输入序列的关键部分,为后续的 Transformer 架构奠定了基础。

CNN 虽然在计算机视觉领域取得了突破性进展,但同一时间线人工智能还面临着另一个重要挑战: 序列数据的处理

序列数据指的是那些具有前后依赖关系的数据,典型的比如文本和语音。与图像处理不同,这类数据的特征不仅体现在其内容上,更重要的是元素之间的顺序关系和上下文联系。

4.3.1 序列数据的核心挑战

顺序敏感与长距离依赖

图像的语义主要来自于局部特征的组合,想象一张猫的照片,无论你从上往下、从右往左等从哪个角度观察,都能识别出这是一只猫,CNN 的卷积核可以在图像的任何位置检测特征,哪怕图片中的某个特征位置变了,但是特征的有效性是基本不变的,卷积核都可以检测到。

但序列数据则完全不同。以文本语言为例,“我喜欢你”和“你喜欢我”用的是同样的词,但交换位置后表达的含义截然不同。词序决定了语义,这就是顺序敏感性。

长距离依赖

更复杂的还有长距离依赖问题。比如下面这个句子:

由于昨天突然下雨,原本计划在公园举行的、邀请了很多同学朋友参加的、准备了很久的生日聚会不得不取消了。

在这个句子中,开头的“由于昨天突然下雨”和结尾的“不得不取消了”构成因果关系,但它们之间隔着一长串修饰成分。机器需要“记住”前面的原因才能理解后面的结果。

这句话中,“下雨”和“取消”之间存在明显的因果关系,但被大量修饰语分隔。机器需要具备“长记忆“的能力,才能正确理解这种跨度较大的语义关联。

变长序列处理

序列数据的另一个显著特点是长度的不确定性。从简短的 5-10 个词组成的短句到长达数千词的文档。传统的全连接神经网络需要固定长度的输入向量。虽然 CNN 可以处理不同尺寸的图像输入,但其中全连接层的输入也是固定的大小。序列数据的长度变化往往更加极端且不规律,如何让神经网络优雅地处理这种任意长度的序列也是一个关键问题。

上下文依赖

语义理解也是高度依赖上下文的,同一个词在不同语境下可能表达完全不同的含义。比如”苹果很甜“中的苹果指水果,而“苹果公司很棒”中的苹果指科技公司。机器需要建立强大的上下文理解能力,才能准确把握词义的细微变化。

4.3.2 循环神经网络RNN

尽管 CNN 在图像处理领域取得了巨大成功,但其核心特点决定了它并不适合处理序列数据。CNN 主要依赖于局部特征提取,这意味着它只能关注输入的局部区域进行处理。同时,它具有位置无关性的特点,因为特征可以在任何位置被同样地检测到。此外,虽然 CNN 可以处理不同尺寸的图像输入,但在网络末端的全连接层仍需要固定大小的输入,这在某种程度上限制了它的灵活性。

而序列数据的处理需求与 CNN 的这些特点形成了鲜明的对比。序列数据需要理解元素之间的全局依赖关系,特别是那些长距离的关联。在序列数据中,位置信息变得极其重要,因为元素的顺序直接影响着整体的语义。同时,序列数据处理还需要能够灵活应对不同长度的输入。

这些本质的差异使得 CNN 难以胜任序列数据处理任务。这种不匹配就像是用专门切菜的刀来写毛笔字,虽然都是工具,但并不适合完成特定的任务。

循环神经网络(Recurrent Neural Network,简称RNN) 的出现,为序列数据处理提供了一个专门的解决方案。事实上,RNN 这个概念和 CNN 一样都早在 20 世纪 80 年代就已经被提出,同样都是深度学习复兴后才发挥了作用。

RNN 的核心思想很简单,就是让神经网络具备记住信息的能力。

想象你在看一部连续剧,每一集的内容都会影响你对下一集的理解。RNN 就是这样工作的,它会把之前看到的信息存储在一个叫做 隐状态(Hidden State) 的地方,然后在处理新信息时把这些记忆调用出来。 与传统的神经网络不同,RNN 有一个关键特征,它有一个指向自己的连接,这就是“循环”的来源(如图4-19)。

图 4-19 RNN基本结构图图 4-19 RNN基本结构图

如果我们把 RNN 的结构简化一下和传统的前馈神经网络做个对比,其实就可以非常明显的看出它们结构上的差异。传统前馈神经网络信息只能单向流动,从输入到输出,每次处理都是独立的,没有记忆能力。而循环神经网络信息可以循环流动,形成记忆机制,当前处理会受到历史信息的影响(如图 4-20)。

前馈神经网络(Feedforward Neural Network,简称FNN),文中提到的基础神经网络无明确指出是何神经网络的均为前馈神经网络。

图 4-20 FNN(左)RNN(中)RNN时间展开图(右)图 4-20 FNN(左)RNN(中)RNN时间展开图(右)

图 4-20 右半部分按时间线展开后的RNN中可以看到,每个时间步都有相同的网络结构,隐状态h在时间步之间传递信息,每个时间步都可以产生输出。如隐藏层节点 hth_{t} 除了依赖当前的输入 xtx_t 之外,还会依赖前一时间隐藏层 ht1h_{t-1},即 RNN 的隐藏层不仅接收当前输入,还接收来自上一时间步的隐状态,以此形成了信息的循环流动。

结合上图中的时间展开图,我们举个例子看RNN如何理解“我喜欢你”这句话:

  • 时间步 1:输入“我” → RNN 记住:主语是第一人称
  • 时间步 2:输入“喜欢” → RNN 记住:动作是喜欢,主语是我
  • 时间步 3:输入“你” → RNN 理解:完整句子是“我喜欢你”

当然你也可以想象自己是一个接力赛跑者,每个时间步就是一个接力点:

  • 你从上一个跑者那里接过接力棒(上一时间步的隐状态)
  • 同时你还要处理当前赛段的路况信息(当前输入)
  • 你根据接力棒上的信息和当前路况,调整自己的跑步策略(计算新的隐状态)
  • 最后你把更新后的信息写在接力棒上,传给下一个跑者(传递隐状态给下一时间步)

这种机制让 RNN 能够在处理序列时保持对历史信息的“记忆”,从而理解上下文关系。

4.3.3 早期 RNN 的致命缺陷

需要注意的是,RNN 的记忆并不是无限的。隐状态的维度是固定的,这意味着它必须将所有历史信息压缩到这个固定大小的向量中。就像用一个固定大小的行李箱装东西,刚开始时,行李箱很空,可以装下所有重要物品,随着物品增多,你必须选择保留最重要的,丢弃不太重要的,到最后,行李箱装满了,每放入新物品都要丢弃一些旧物品。

这也就意味着虽然 RNN 具备了处理序列的能力,但还是有限制的,特别是在处理超长序列时,早期信息就会被“遗忘”,这是早期RNN的致命缺陷。

4.3.4 LSTM——解决遗忘问题

早期 RNN 的遗忘问题就像是一个健忘的人,看到新的信息时很容易忘记之前的重要内容。这类似于人类在阅读长篇文章时,容易遗忘开头的重要信息。从技术角度来说,这叫梯度消失问题:在网络训练过程中,随着序列长度的增加,较早时间步的梯度信号会呈指数衰减,导致网络难以捕获长距离依赖关系。

为了解决这个关键问题,早在 1997 年 霍克赖特(Hochreiter)施密德胡贝(Schmidhuber) 就提出了一个创新性的解决方案:长短期记忆网络(Long Short-Term Memory,简称LSTM)。LSTM 通过精妙的门控机制设计,成功克服了传统 RNN 的局限性。

LSTM 的核心在于其三重门控机制,这种设计堪比一个智能化的信息管理系统:

  • 遗忘门(Forget Gate): 通过 sigmoid 函数计算一个 0 到 1 之间的权重向量,决定需要遗忘的历史信息比例。这让网络能够主动“遗忘”不再相关的历史信息。
  • 输入门(Input Gate): 控制当前时刻有多少新信息需要被存储到细胞状态中。它包含两个部分:sigmoid 层决定更新哪些值,tanh 层创建新的候选值。
  • 输出门(Output Gate): 确定细胞状态中的哪些信息将作为当前时刻的输出。这个机制让网络能够有选择地展示内部状态。

值得注意的是,LSTM 的演进经历了多次改进。最初的版本只包含输入门和输出门,直到 2000 年,Felix Gers 等人增加了遗忘门,这一改进显著提升了 LSTM 的性能。

这种精密的门控架构使 LSTM 能够有效管理长达数百个时间步的信息流动,极大地提升了模型在长序列建模任务上的表现。更形象地说,这就像给神经网络装配了一个智能化的记忆管理系统,既能保持长期记忆的稳定性,又能灵活处理短期信息的更新。

尽管 LSTM 的理论基础早在 90 年代末就已建立,但由于计算资源限制和数据规模不足,其真正的潜力直到 2010 年后才得以充分释放。随着计算能力的显著提升、大规模数据的可获得性提高,以及架构优化和训练技术的进步,LSTM 在语音识别、机器翻译、文本生成等序列建模任务中取得了突破性进展,也成为了深度学习领域的关键技术之一。

4.3.5 RNN的根本局限

随着应用的深入,RNN 的根本局限开始显现。这些限制不是小修小补就能解决的,而是架构本身带来的根本性问题。

串行计算的桎梏

RNN 最大的问题是必须串行处理序列。无论是处理“我喜欢你”这样的短句,还是处理长篇文档,RNN 都必须按顺序逐个处理。

  • 时间步 1: 处理第 1 个词 → 生成状态 h1
  • 时间步 2: 等待 h1 完成,处理第 2 个词 → 生成状态 h2
  • 时间步 3: 等待 h2 完成,处理第 3 个词 → 生成状态 h3

这种串行处理机制存在几个技术限制:

尽管现代 GPU 拥有数以千计的并行计算核心,能够同时处理海量数据,但 RNN 的顺序依赖特性使其无法充分利用这种并行计算优势。比如处理一个句子时,必须等第一个词处理完才能处理第二个词。

由于 RNN 需要严格按照时间步骤顺序处理,当面对长文本时,计算时间会随序列长度线性增长。举例来说,处理一篇包含 1000 个词的文档需要严格执行 1000 个连续的计算步骤,这种计算模式在大规模应用场景下显得极其耗时。

在需要即时反馈的应用场景中(如智能对话系统、实时翻译等),RNN 的串行处理特性会导致明显的响应延迟。因为系统必须等待整个输入序列被逐个处理完毕才能生成完整的输出,这严重影响了用户体验。

信息瓶颈问题

在机器翻译等任务中,RNN 还面临着信息瓶颈的挑战。传统的 RNN 需要把整个源语句的信息压缩到一个固定大小的向量中,然后再从这个向量生成目标语句。这就像让一个人读完一本厚厚的书,然后用一页纸写摘要,再根据这一页摘要重新写出整本书的内容。信息损失是不可避免的,特别是对于长句子,RNN 总是“顾头不顾尾”,开头部分还算准确,到了后面就开始出现各种错误。

长距离依赖问题

尽管 LSTM 以一种巧妙的机制大大改善了 RNN 的记忆能力,但它仍然难以处理非常长距离的依赖关系。

可解释性的缺失

RNN 的记忆机制虽然强大,但往往会很缺乏可解释性。当模型做出某个预测时,很难准确解释它是基于哪些历史信息做出的判断。这个黑盒特性在很多应用场景中是不可接受的,特别是在医疗、金融等对可靠性要求极高的领域。

4.3.6 注意力机制萌芽

面对 RNN 的诸多局限性,研究者们开始探索新的解决方案。其中最具革命性的想法之一就是 注意力机制(Attention Mechanism)

在机器翻译任务中,传统的 RNN 翻译模型采用 “编码器-解码器” 架构:

  • 编码器: 将源语言句子逐词处理,最终将整个句子的信息压缩成一个固定大小的向量
  • 解码器: 基于这个压缩向量,逐词生成目标语言的翻译

这种架构的问题显而易见:无论源语言句子有多长、多复杂,都必须压缩到同样大小的向量中。就像要把一本厚厚的书的所有内容都写在一张便利贴上——信息损失是不可避免的。

更糟糕的是,当解码器生成目标语言的每个词时,它只能“看到”这个压缩向量,无法知道源语言中哪些部分与当前要生成的词最相关。

观察人类翻译师的工作过程,我们会发现一个有趣的现象:翻译师并不会先把整个源语言句子“背下来”,然后完全凭记忆进行翻译。相反,他们会:

  • 整体理解源语言句子的意思
  • 在翻译每个部分时,会回头查看源语言的相应部分
  • 根据上下文动态调整对源语言不同部分的关注程度

比如,在翻译“我昨天在图书馆里看了一本很有趣的书”这句话时:

  • 翻译“I”时,主要关注“我”
  • 翻译“read”时,主要关注“看了”
  • 翻译“an interesting book”时,主要关注“一本很有趣的书”
  • 翻译“in the library”时,主要关注“在图书馆里”

这种“动态关注”的能力正是传统RNN所缺乏的。

既然解码器在生成每个词时都需要参考源语言的不同部分,为什么不让它直接“看到”整个源语言句子,并学会动态地选择关注哪些部分呢?

这就是注意力机制的核心思想:让模型学会自主决定在每个时刻应该关注输入的哪些部分。

用更形象的比喻,传统 RNN 就像一个近视眼的人在阅读,只能看清眼前的一小块内容。而注意力机制则给了模型一个“聚光灯”,让它可以灵活地将光束投射到输入序列的任何位置,照亮当前最需要关注的部分。 注意力机制类似的架构思想其实可以追溯到 90 年代,但直到 2014 年,由于主流RNN架构存在缺陷,注意力机制被正式提出,各个研究团队开始将注意力机制和 RNN 结合以解决 RNN 架构的一些问题。

我们还是以翻译“我昨天在图书馆里看了一本很有趣的书”为例了解一下注意力机制的工作原理。

在传统的 RNN 翻译任务中,编码器会把整个中文句子的信息压缩成一个向量(就像把所有主要信息写在一张小纸条上)。当你要翻译每个英文单词时,只能看这张纸条,而看不到原始的中文句子。这就好比有人用 5 秒钟快速告诉你一个复杂故事的“要点”,然后要求你根据这个要点完整复述整个故事,显然会漏掉很多细节,特别是故事很长的时候。

注意力机制则不同,它让你在翻译每个英文单词时,都可以“回头看”整个中文句子,并且智能地把注意力集中在最相关的中文词汇上。

让我们看看具体是如何做的,首先第一步是计算相关性分数(谁最重要?)。

当翻译到英文单词“library”时,系统会问:中文句子里哪个词与"library"最相关?系统会计算当前要翻译的词与中文每个词之间的相关性,显然,“图书馆”与“library”的相关性最高,这符合我们的直觉:

要翻译: "library"
中文句子: "我" "昨天" "在" "图书馆" "看了" "一本" "有趣的" "书"

相关性计算:
"我" ← → "library"      相关性分数: 0.1
"昨天" ← → "library"    相关性分数: 0.2  
"在" ← → "library"      相关性分数: 1.5
"图书馆" ← → "library"   相关性分数: 8.7  (最高!)
"看了" ← → "library"     相关性分数: 0.3
"一本" ← → "library"     相关性分数: 0.1
"有趣的" ← → "library"   相关性分数: 0.1
"书" ← → "library"       相关性分数: 0.8

接着第二步,将分数转换为注意力权重(分配注意力)。

光有分数还不够,我们需要把这些分数转换成注意力权重。这就像把你的注意力分成100%,然后分配给不同的中文词汇。技术上称之为 归一化(Normalization),即将数据按比例缩放至特定范围或统一标准的数据预处理技术。

使用 softmax 函数将相关性分数进行归一化:

原始分数: [0.1, 0.2, 1.5, 8.7, 0.3, 0.1, 0.1, 0.8]

转换后的注意力权重:
"我": 0.02        (2%的注意力)
"昨天": 0.02      (2%的注意力)  
"在": 0.08        (8%的注意力)
"图书馆": 0.78    (78%的注意力) ← 主要关注点!
"看了": 0.03      (3%的注意力)
"一本": 0.02      (2%的注意力)
"有趣的": 0.02    (2%的注意力)
"书": 0.04        (4%的注意力)

总和 = 1.00 (100%的注意力都分配完了)

这就像你在翻译”library”时,78%的注意力集中在”图书馆”上,8% 的注意力关注”在”(因为”在图书馆”是一个短语),其余词汇只占很少的注意力。

第三步,生成上下文向量(融合信息)。

现在我们知道了应该关注哪些词以及关注的程度,接下来就要把这些信息融合起来。

想象每个中文词都是一个装满信息的盒子,我们要根据注意力权重从每个盒子里取出相应份量的信息:

上下文向量 = 0.02×"我"的信息 + 0.02×"昨天"的信息 + 0.08×"在"的信息 
           + 0.78×"图书馆"的信息 + 0.03×"看了"的信息 + 0.02×"一本"的信息
           + 0.02×"有趣的"的信息 + 0.04×"书"的信息

由于“图书馆”的权重是 0.78,所以最终的上下文向量主要包含了“图书馆”的信息,少量包含“在”的信息,以及其他词的微量信息。

经过这三步,我们就可以看到翻译整个句子时的注意力变化:

中文: "我 昨天 在 图书馆 看了 一本 有趣的 书"

翻译"I"时,主要关注: "我"(85%) + "昨天"(10%) + 其他(5%)
翻译"yesterday"时,主要关注: "昨天"(80%) + "我"(15%) + 其他(5%)  
翻译"at"时,主要关注: "在"(70%) + "图书馆"(20%) + 其他(10%)
翻译"the"时,主要关注: "图书馆"(60%) + "一本"(25%) + 其他(15%)
翻译"library"时,主要关注: "图书馆"(78%) + "在"(8%) + 其他(14%)
翻译"read"时,主要关注: "看了"(75%) + "书"(15%) + 其他(10%)
翻译"an"时,主要关注: "一本"(80%) + "书"(10%) + 其他(10%)
翻译"interesting"时,主要关注: "有趣的"(85%) + "书"(10%) + 其他(5%)
翻译"book"时,主要关注: "书"(75%) + "一本"(15%) + 其他(10%)

不需要把所有信息压缩到一个小向量里,每翻译一个词,都能找到最相关的源语言信息,这正是人类翻译师的工作方式,甚至我们可以清楚看到模型在关注什么。这就是注意力机制的魔法,它让机器学会了像人类一样“有重点的”关注信息,而不是盲目地处理所有信息。

这种“动态关注”的思想不仅适用于机器翻译,还可以应用到几乎所有涉及序列处理的任务中,文本摘要、问答系统、图像描述生成等等。

注意力机制不仅仅是一个技术改进,更代表了一种全新的信息处理方式,传统模型只能被动地接收固定的输入表示,而注意力机制让模型学会主动选择和组合信息。不再强行将所有信息压缩到固定向量,而是在需要时动态聚焦到最相关的局部信息。模型的“关注焦点”不再是固定的,而是根据具体任务和上下文动态调整。

但这个时期的注意力机制只是作为 RNN 架构的补充使用的,主要还是为了辅助 RNN 解决一些问题。

4.3.7 代码生成的早期探索

编程语言本质上也是一种语言系统,那么既然 RNN 能够处理自然语言,它是否也能理解和处理编程语言呢?

这个想法其实并不荒谬。编程语言和自然语言有许多共同点:它们都具有规范的语法结构、明确的语义,并且都由符号序列构成。相比之下,编程语言的优势在于其更严格的语法规则、零歧义性,以及更清晰的结构层次(如函数、类、模块等),同时具备明确的执行逻辑和语义关系。

从 2014 年开始,研究团队逐步开始尝试使用 RNN 模型实现代码补全功能。例如,当我们编写如下代码时:

def fibonacci(n):
  if n <= 1:
    return [空白处]

模型能够智能推荐最合适的代码补全选项。虽然这个功能看似简单,但它代表了神经网络首次真正开始理解代码结构和语义的重要突破。随着越来越多的团队投入代码生成研究,他们发现单纯将代码视为字符序列或词序列会损失重要的结构信息。为解决这个问题,研究者们引入了抽象语法树(AST)的概念,示例如下:

# 原始代码
x = a + b

# 对应的AST结构
Assignment
├── Variable: x
└── BinaryOp: +
  ├── Variable: a
  └── Variable: b

通过遍历 AST,我们可以获得一个保留了完整代码结构信息的序列表示,这使得 RNN 能够更准确地理解代码的层次关系和语法结构。

到 2016 年左右,RNN 在代码理解已经领域取得了显著突破。研究人员开发出了多种实用的代码处理工具:可以识别功能相似的代码片段、根据自然语言描述检索相应的代码实现、自动修复简单的程序错误,甚至能够为代码生成清晰的文档说明。这些进展表明,神经网络已经具备了基础的程序代码理解和处理能力。

尽管这些应用仍处于初期阶段,但已经展现出AI辅助编程的广阔前景。更重要的是,这些探索为后来更强大的代码生成模型奠定了基础。

Edit on GitHub

Last updated on