标准循环神经网络,例如LSTM和GRU,以单向方式处理序列,通常按时间顺序从开始到结束。在任意给定时间步$t$,隐藏状态$h_t$概括了来自过去的输入$x_1, x_2, ..., x_t$的信息。尽管这反映了我们经常感受时间相关现象的方式,但对于某些任务而言可能存在局限。考虑理解句子中一个词的含义。有时,正确解释一个词所需的上下文会在该词 之后 出现。例如,在句子“他与棒球队一起吃了一个蝙蝠”中,知道“棒球队”这些词有助于消除“bat”的歧义(更可能是运动器材,而不是动物)。一个从左到右处理的标准RNN,在看到澄清上下文之前就已经处理了“bat”。在这里,双向循环神经网络(BiRNN)提供了优势。其主要思想简单明了:同时使用两个独立的循环层,以两个方向处理序列。前向层: 该层从第一个元素到最后一个元素($t=1$到$T$)处理输入序列。它在时间$t$的隐藏状态,记为$\overrightarrow{h_t}$,捕捉过去上下文($x_1, ..., x_t$)的信息。后向层: 该层反向处理输入序列,从最后一个元素到第一个元素($t=T$到$1$)。它在时间$t$的隐藏状态,记为$\overleftarrow{h_t}$,捕捉未来上下文($x_t, ..., x_T$)的信息。这两个层独立运行,每个都维护自己的一组权重和隐藏状态。它们可以由简单的RNN、LSTM或GRU单元组成。架构与信息流在每个时间步$t$,BiRNN会产生一个输出,其中包含来自前向和后向处理的信息。将两个层的信息组合起来的最常见方法是在该时间步连接它们各自的隐藏状态。The overall hidden state or output representation $y_t$ at time step $t$ can be formed as:$$ y_t = g([\overrightarrow{h_t} ; \overleftarrow{h_t}]) $$这里,$[\overrightarrow{h_t} ; \overleftarrow{h_t}]$表示前向隐藏状态$\overrightarrow{h_t}$与后向隐藏状态$\overleftarrow{h_t}$的连接。函数$g$可以是一个恒等函数(直接使用连接后的状态),或者它可能涉及进一步的处理,例如将连接后的向量通过一个全连接层,具体取决于模型架构和任务。其他组合方法,如求和或平均,也存在,但不如连接方法常见。下图展示了这种结构:digraph G { rankdir=LR; node [shape=box, style=rounded, fontname="sans-serif", fillcolor="#e9ecef", style=filled]; edge [fontname="sans-serif"]; subgraph cluster_input { label = "输入序列"; style=filled; fillcolor="#f8f9fa"; rank=same; x1 [label="x₁", shape=plaintext]; x2 [label="x₂", shape=plaintext]; xt [label="...", shape=plaintext]; xT [label="x<0xE2><0x82><0x93>", shape=plaintext]; // x_T x1 -> x2 -> xt -> xT [style=invis]; } subgraph cluster_forward { label = "前向RNN (LSTM/GRU)"; fillcolor="#a5d8ff"; style=filled; node [fillcolor="#d0ebff"]; fh1 [label=< <B>→</B><BR/>h<SUB>1</SUB> >]; // →h₁ fh2 [label=< <B>→</B><BR/>h<SUB>2</SUB> >]; // →h₂ fht [label=< ... >]; fhT [label=< <B>→</B><BR/>h<SUB>T</SUB> >]; // →h_T fh1 -> fh2 -> fht -> fhT; } subgraph cluster_backward { label = "后向RNN (LSTM/GRU)"; fillcolor="#ffec99"; style=filled; node [fillcolor="#fff9db"]; bh1 [label=< <B>←</B><BR/>h<SUB>1</SUB> >]; // ←h₁ bh2 [label=< <B>←</B><BR/>h<SUB>2</SUB> >]; // ←h₂ bht [label=< ... >]; bhT [label=< <B>←</B><BR/>h<SUB>T</SUB> >]; // ←h_T bhT -> bht -> bh2 -> bh1; } subgraph cluster_output { label = "组合输出"; style=filled; fillcolor="#f8f9fa"; rank=same; y1 [label="y₁", shape=oval, fillcolor="#ced4da"]; y2 [label="y₂", shape=oval, fillcolor="#ced4da"]; yt [label="...", shape=plaintext]; yT [label="y<0xE2><0x82><0x93>", shape=oval, fillcolor="#ced4da"]; // y_T y1 -> y2 -> yt -> yT [style=invis]; } // 输入连接 x1 -> fh1; x1 -> bh1; x2 -> fh2; x2 -> bh2; xt -> fht; xt -> bht; xT -> fhT; xT -> bhT; // 输出连接(暗示连接操作) {rank=same; fh1; bh1} -> y1 [style=dashed]; {rank=same; fh2; bh2} -> y2 [style=dashed]; {rank=same; fht; bht} -> yt [style=dashed]; {rank=same; fhT; bhT} -> yT [style=dashed]; }一个双向RNN使用两个独立的循环层处理输入序列$x_1, ..., x_T$。前向层根据过去信息计算隐藏状态$\overrightarrow{h_t}$,而后向层根据未来信息计算$\overleftarrow{h_t}$。每一步的最终输出$y_t$结合了$\overrightarrow{h_t}$和$\overleftarrow{h_t}$,通常通过连接操作。优点与缺点BiRNN的主要优点是它们能够整合来自两个方向的上下文。这通常会提高在那些元素理解依赖于其周围上下文的任务上的表现。例子包括:自然语言处理: 情感分析、命名实体识别(NER)、词性标注和机器翻译经常受益于双向上下文。语音识别: 理解音素可能取决于周围的声音。生物信息学: 分析DNA或蛋白质结构等序列。然而,BiRNN也伴随着一些考量:对完整序列的依赖: 后向传递需要从末尾到开头处理序列。这意味着在计算完成之前,必须提供整个输入序列。因此,BiRNN通常不适合实时应用,在这些应用中,预测必须随着数据到达而增量进行,且无法访问未来的输入(例如,仅基于过去价格的实时股票价格预测)。计算成本增加: 使用两个循环层而非一个,与相同隐藏层大小和类型的单向RNN相比,参数数量和训练及推断所需的计算量大约增加一倍。何时使用双向模型在以下情况选择双向架构:任务涉及处理整个序列,其中来自过去和未来元素的上下文对于在每一步进行预测或分类有益(例如,像命名实体识别这样的序列标注)。任务需要根据所有元素对整个序列进行分类或概括(例如,对完整评论的情感分析)。提前需要完整序列的限制对于该应用来说是可接受的。对于需要真正的在线处理或预测,且在预测时无法获得未来输入的任务,应避免使用双向架构。在此类情况下,标准的单向RNN是合适的选择。在理解了双向处理的思想和用途之后,我们现在将探讨如何使用流行的深度学习框架实现标准和双向的LSTM和GRU层。