标准的循环层,无论是简单的RNN、LSTM还是GRU,都以单一方向处理序列,通常从序列的开始到结束($t=1$ 到 $T$)。在任何给定时间步 $t$,隐藏状态 $h_t$ 仅概括了过去输入的信息($x_1, ..., x_t$)。虽然这能有效地捕捉历史背景信息,但许多序列建模任务也能从理解特定元素之后的上下文信息中获益。考虑情感分析或命名实体识别之类的任务。一个词的含义或它在句子中的作用,往往不仅取决于它之前的内容,也取决于它之后的内容。比如,识别“bank”是指金融机构还是河岸,可能需要查看句子中后面的信息。这就是双向RNN(BiRNN)的用武之地。其主要思路很简单:即不只使用一个RNN向前处理序列,而是使用两个独立的RNN。前向层: 从 $t=1$ 到 $T$ 处理输入序列,生成一系列前向隐藏状态:$\overrightarrow{h_1}, \overrightarrow{h_2}, ..., \overrightarrow{h_T}$。后向层: 以反向处理输入序列,从 $t=T$ 向下到 $1$,生成一系列后向隐藏状态:$\overleftarrow{h_1}, \overleftarrow{h_2}, ..., \overleftarrow{h_T}$。在每个时间步 $t$,最终的隐藏状态表示会结合来自两个方向的信息。最常用的组合方法是拼接:$$ h_t = [\overrightarrow{h_t} ; \overleftarrow{h_t}] $$这里,$[\cdot ; \cdot]$ 表示向量拼接。如果前向层和后向层各有 units 个隐藏单元,那么每个时间步的最终组合隐藏状态 $h_t$ 将具有 2 * units 维。这种组合状态能捕捉相对于当前时间步 $t$ 的过去和未来上下文信息。digraph G { rankdir=LR; node [shape=box, style=rounded, fontname="sans-serif", color="#495057", fillcolor="#e9ecef", style="filled, rounded"]; edge [color="#495057"]; subgraph cluster_forward { label = "前向层 (->)"; style=dashed; color="#1c7ed6"; node [fillcolor="#a5d8ff"]; f_h_prev [label="h(t-1)->"]; f_x_t [label="x(t)"]; f_rnn [label="RNN/LSTM/GRU"]; f_h_t [label="h(t)->"]; f_x_t -> f_rnn; f_h_prev -> f_rnn [label=" Wf "]; f_rnn -> f_h_t; } subgraph cluster_backward { label = "后向层 (<-)"; style=dashed; color="#f76707"; node [fillcolor="#ffd8a8"]; b_h_next [label="<-h(t+1)"]; b_x_t [label="x(t)"]; b_rnn [label="RNN/LSTM/GRU"]; b_h_t [label="<-h(t)"]; b_x_t -> b_rnn; b_h_next -> b_rnn [label=" Wb "]; b_rnn -> b_h_t; } subgraph cluster_output { label = "组合输出"; style=dashed; color="#495057"; node [fillcolor="#dee2e6", shape=ellipse]; h_t [label="h(t) = [h(t)-> ; <-h(t)]"]; } f_h_t -> h_t [style=invis]; b_h_t -> h_t [style=invis]; {rank=same; f_rnn; b_rnn;} {rank=same; f_h_t; b_h_t;} // Invisible edges for alignment f_h_prev -> b_h_next [style=invis]; f_x_t -> b_x_t [style=invis]; // Explicit connections to combined output f_h_t -> h_t [lhead=cluster_output, minlen=2, color="#1c7ed6"]; b_h_t -> h_t [lhead=cluster_output, minlen=2, color="#f76707"]; }双向RNN在时间步 $t$ 的图示。它包含两个独立的RNN层,处理输入 $x_t$ 以及前一个前向状态 $\overrightarrow{h}{t-1}$ 和下一个后向状态 $\overleftarrow{h}{t+1}$。它们的输出通常会被拼接起来,形成最终的隐藏状态 $h_t$。在框架中实现双向层深度学习库提供了便捷的方法来创建双向循环层。TensorFlow (Keras API)在Keras中,您可以使用 tf.keras.layers.Bidirectional 包装器,它将一个循环层(如 LSTM 或 GRU)实例作为其主要参数。import tensorflow as tf # 假设输入形状 = (batch_size, timesteps, features) # 示例: (32, 20, 10) -> 20个时间步,每个时间步10个特征 # 创建一个双向LSTM层 # hidden_units 定义了每个方向输出空间的维度 hidden_units = 64 lstm_layer = tf.keras.layers.LSTM(hidden_units, return_sequences=True) bidirectional_lstm = tf.keras.layers.Bidirectional(lstm_layer) # 如果输入形状是 (32, 20, 10) 且 hidden_units=64: # 输出形状将是 (32, 20, 128),因为前向 (64) 和后向 (64) # 输出默认通过拼接合并 (merge_mode='concat')。 # 在Sequential模型中的使用示例: model = tf.keras.Sequential([ tf.keras.layers.Input(shape=(20, 10)), # 显式定义输入形状 bidirectional_lstm, # 添加后续层,例如用于分类的全连接层 tf.keras.layers.Dense(1, activation='sigmoid') ]) model.summary()Bidirectional 包装器负责创建所提供层的前向和后向实例,并合并它们的输出。默认的 merge_mode 是 'concat',它沿着最后一个轴拼接前向和后向输出。其他选项包括 'sum'、'mul' 和 'ave',但拼接是最常用的。如果包装层中的 return_sequences=True,则 Bidirectional 层会输出每个时间步的组合隐藏状态。如果 return_sequences=False,它将只输出最终的组合隐藏状态(即最后一个前向状态 $\overrightarrow{h_T}$ 和第一个后向状态 $\overleftarrow{h_1}$ 的拼接)。PyTorch在PyTorch中,双向性作为 LSTM 和 GRU 层初始化器中的一个参数直接得到支持。import torch import torch.nn as nn # 假设输入形状: (batch_size, seq_len, input_size) # 示例: (32, 20, 10) # 创建一个双向GRU层 input_size = 10 hidden_size = 64 # 定义每个方向的维度 num_layers = 1 # 堆叠层数 (可以 > 1) # 设置 bidirectional=True bi_gru_layer = nn.GRU(input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, # 输入/输出张量中批次维度优先 bidirectional=True) # 输入张量示例 batch_size = 32 seq_len = 20 input_tensor = torch.randn(batch_size, seq_len, input_size) # 前向传播 # 输出形状: (batch_size, seq_len, num_directions * hidden_size) -> (32, 20, 2 * 64) # hn 形状: (num_layers * num_directions, batch_size, hidden_size) -> (1 * 2, 32, 64) output, hn = bi_gru_layer(input_tensor) print("输出形状:", output.shape) print("最终隐藏状态形状:", hn.shape) # 'output' 张量包含每个时间步拼接的前向和后向隐藏状态。 # output[batch, t, :hidden_size] 是时间 t 的前向状态 # output[batch, t, hidden_size:] 是时间 t 的后向状态 # 'hn' 张量包含每个层和方向的最终隐藏状态。 # 对于单层BiGRU: # hn[0, :, :] 是最终的前向隐藏状态 h_T-> # hn[1, :, :] 是最终的后向隐藏状态 h_1<- (来自反向序列的开始)将 bidirectional=True 设置为 True 会自动使输出特征的有效大小加倍 (num_directions * hidden_size),因为它拼接了前向和后向隐藏状态。batch_first=True 参数很实用,因为它使张量维度与常见做法(批次、序列、特征)保持一致。应用场景与注意事项双向RNN对于在做出预测前即可获得整个输入序列的离线任务尤其有效。它们在以下任务上通常比单向RNN表现更好:自然语言处理: 情感分析、命名实体识别(NER)、词性标注(POS)、问答和机器翻译(尤其是在编码器-解码器模型的编码器部分)。理解完整的句子上下文非常有益。生物信息学: 蛋白质结构预测或基因序列分析。然而,BiRNN通常不适用于在线任务或实时预测(例如仅基于过去数据的股市预测),因为反向传播需要未来时间步的信息,而这些信息在实时场景中是无法获得的。与具有相同隐藏大小的单向层相比,实现双向层会引入大约两倍的参数和计算量,因为它涉及到训练两个独立的RNN。这是在使用双向上下文可能带来的性能提升与额外成本之间需要权衡的因素。