以动画形式解读双向LSTM算法

以动画形式解读双向LSTM算法 1. 引言序列数据与上下文的重要性在人工智能的世界里序列数据无处不在一段文本、一句语音、一串股票价格、一段视频帧……这些数据的共同点是顺序性前一个元素与后一个元素之间存在依赖关系。例如在句子“我喜欢吃苹果”中“苹果”的含义依赖于前面的动词“吃”而如果句子是“我喜欢用苹果手机”“苹果”的含义则完全不同。这种依赖关系往往不是单向的——后文也可能帮助我们理解前文。比如“他跑到银行因为那里很安全”中“银行”指代的是金融机构而“他跑到银行因为要取钱”中“银行”仍然是金融机构但如果后半句是“因为河边的风景很美”那么“银行”就变成了河岸。显然要准确理解一个词需要同时看它的左边和右边。传统的机器学习方法如词袋模型、n-gram无法有效捕捉长距离的上下文依赖。循环神经网络RNN的诞生为处理序列数据提供了天然框架但简单的RNN面临梯度消失/爆炸问题难以建模长期依赖。长短时记忆网络LSTM通过精巧的门控机制解决了这一问题成为处理序列数据的利器。然而标准的LSTM是单向的它只能利用过去的信息来预测当前无法看到未来。双向LSTMBiLSTM则将两个方向相反的LSTM组合起来让每个时间步的输出既能融合历史信息也能融合未来信息从而获得更丰富的上下文表示。本文将以动画形式逐步拆解双向LSTM的内部工作原理从单个LSTM单元的门控机制到双向如何协同工作并用详细的数学公式、代码示例和应用场景帮助你彻底理解这一经典模型。2. 从RNN到LSTM破解长期依赖难题2.1 简单RNN的循环结构一个简单的RNN单元可以表示为httanh⁡(WxhxtWhhht−1bh)ht​tanh(Wxh​xt​Whh​ht−1​bh​)其中xtxt​ 是当前输入ht−1ht−1​ 是上一个时间步的隐藏状态htht​ 是当前隐藏状态。这个结构通过循环连接使得信息可以从上一个时刻传递到下一个时刻。2.2 梯度消失的困境在反向传播过程中误差信号需要沿着时间步反向传播。由于tanh⁡tanh的导数小于1大部分时候多次连乘会导致梯度指数级衰减使得网络难以学习到远距离的依赖关系。例如在句子“我出生于法国……我会说法语”中“法语”和“法国”之间相隔很多词简单RNN很难学到这种关联。2.3 LSTM的引入1997年Hochreiter和Schmidhuber提出了LSTM通过引入门控机制和细胞状态cell state让信息可以长距离流动而不衰减。LSTM的核心思想是让网络学会如何选择性地记住或遗忘信息。3. LSTM的微观世界门控机制与细胞状态为了更好地理解双向LSTM我们首先必须深入LSTM单元的内部。以下是一个标准LSTM单元的数学描述假设没有peephole连接3.1 遗忘门Forget Gate决定从上一个细胞状态中丢弃什么信息。它读取 ht−1ht−1​ 和 xtxt​输出一个0到1之间的数值给细胞状态 Ct−1Ct−1​ftσ(Wf⋅[ht−1,xt]bf)ft​σ(Wf​⋅[ht−1​,xt​]bf​)其中 σσ 是sigmoid函数。3.2 输入门Input Gate决定将多少新信息存入细胞状态。它由两部分组成一个sigmoid层决定要更新哪些值itσ(Wi⋅[ht−1,xt]bi)it​σ(Wi​⋅[ht−1​,xt​]bi​)一个tanh层创建新的候选值向量 C~tC~t​C~ttanh⁡(WC⋅[ht−1,xt]bC)C~t​tanh(WC​⋅[ht−1​,xt​]bC​)3.3 更新细胞状态将旧的细胞状态 Ct−1Ct−1​ 更新为新的细胞状态 CtCt​Ctft⊙Ct−1it⊙C~tCt​ft​⊙Ct−1​it​⊙C~t​这里 ⊙⊙ 表示逐元素相乘。遗忘门控制过去信息的保留程度输入门控制新信息的加入程度。3.4 输出门Output Gate决定基于当前的细胞状态输出什么信息首先sigmoid层决定要输出细胞状态的哪些部分otσ(Wo⋅[ht−1,xt]bo)ot​σ(Wo​⋅[ht−1​,xt​]bo​)然后将细胞状态通过tanh处理得到-1到1的值并与输出门相乘htot⊙tanh⁡(Ct)ht​ot​⊙tanh(Ct​)这样每个LSTM单元都有一个隐藏状态 htht​ 和一个细胞状态 CtCt​两者共同携带信息流向下一时刻。3.5 动画帧描述LSTM单元内部数据流为了直观理解我们可以设计如下动画帧以下用文字描述画面帧1展示一个矩形代表LSTM单元左侧有两个输入箭头xtxt​来自当前输入和 ht−1ht−1​来自上一个隐藏状态。顶部有一个输入箭头 Ct−1Ct−1​上一个细胞状态。帧2ht−1ht−1​ 和 xtxt​ 合并拼接成一个长向量分别进入三个不同的sigmoid单元分别对应遗忘门、输入门、输出门和一个tanh单元候选值。用不同的颜色区分例如遗忘门用红色输入门用绿色输出门用蓝色候选值用紫色。帧3遗忘门输出 ftft​与 Ct−1Ct−1​ 逐元素相乘表示丢弃一部分信息用淡化的 Ct−1Ct−1​ 表示。帧4输入门输出 itit​ 与候选值 C~tC~t​ 逐元素相乘得到新信息。帧5将保留的旧信息和新信息相加得到新的细胞状态 CtCt​。帧6CtCt​ 通过tanh然后与输出门 otot​ 相乘得到新的隐藏状态 htht​。帧7htht​ 和 CtCt​ 从单元右侧输出流向下一时刻。通过这样的动画可以清晰地看到信息如何被门控筛选细胞状态像一条传送带贯穿整个序列。4. 单向的局限为什么要双向单向LSTM在处理序列时每个时间步的隐藏状态 htht​ 只包含了过去的信息包括当前输入但没有包含未来的信息。在很多任务中未来的信息同样重要。例如命名实体识别判断“苹果”是不是公司名可能需要看后面是否有“公司”或“发布了新产品”。机器翻译翻译一个词时需要考虑整个句子才能确定其含义。语音识别当前音素的识别可能依赖于后续音素协同发音现象。双向RNN的初衷正是让网络在每个时间点都能同时获得前后上下文。它通过两个独立的RNN层通常是LSTM或GRU来实现一个按时间顺序处理输入序列正向另一个按时间逆序处理输入序列反向。然后将两个方向的隐藏状态在每一时刻拼接起来作为该时刻的输出。5. 双向LSTM架构全景双向LSTM由两个独立的LSTM层组成它们共享相同的输入序列但方向相反。设输入序列为 x1,x2,…,xTx1​,x2​,…,xT​。前向LSTM从 t1t1 到 tTtT 计算前向隐藏状态 h→tht​ 和细胞状态 C→tCt​。递推公式为h→tLSTMforward(xt,h→t−1,C→t−1)ht​LSTMforward​(xt​,ht−1​,Ct−1​)其中 h→0h0​ 和 C→0C0​ 通常初始化为零向量。后向LSTM从 tTtT 到 t1t1 计算后向隐藏状态 h←tht​ 和细胞状态 C←tCt​。递推公式为h←tLSTMbackward(xt,h←t1,C←t1)ht​LSTMbackward​(xt​,ht1​,Ct1​)其中 h←T1hT1​ 和 C←T1CT1​ 初始化为零向量。在每一个时间步 tt最终的输出隐藏状态 htht​ 是两个方向隐藏状态的拼接ht[h→t;h←t]或者按其他方式组合如相加ht​[ht​;ht​]或者按其他方式组合如相加这个 htht​ 可以用于后续的分类、生成等任务。如果需要序列级别的输出如情感分类通常会将所有时间步的 htht​ 进行池化平均或最大或取最后一个时间步但双向中最后一个时间步的含义需注意。6. 动画演示双向LSTM处理序列的全过程为了让读者更直观地理解双向LSTM我们将设计一个完整的动画剧本包含多帧画面逐步展示从输入到输出的全过程。假设任务是对电影评论进行情感分类正面/负面输入句子为“The movie was not good at all”。我们将逐词处理。6.1 场景设定情感分析任务输入序列单词列表 [The, movie, was, not, good, at, all]每个单词首先通过嵌入层转换为向量 xtxt​例如300维。我们使用一个双向LSTM层每个方向的隐藏状态维度设为128因此每个时间步输出的 htht​ 维度为256。最后将所有 htht​ 平均送入一个全连接层进行二分类。6.2 正向LSTM逐帧解析动画帧说明画面顶部从左到右排列着输入词向量 x1,x2,…,x7x1​,x2​,…,x7​。下方有两排LSTM单元上一排是正向LSTM从左向右流动下一排是反向LSTM从右向左流动。每个LSTM单元内部可展开为门控结构如第3节所述但为简化我们只显示隐藏状态和细胞状态的流动。帧F1初始化展示正向LSTM的初始隐藏状态 h→0h0​ 和细胞状态 C→0C0​ 为零向量位于最左侧。帧F2t1处理“The”输入 x1x1​ 进入正向LSTM单元1。单元1接收 h→0,C→0h0​,C0​计算得到 h→1,C→1h1​,C1​。画面中h→1h1​ 从单元1右侧输出并连接一个箭头指向单元2表示传递给下一时刻。帧F3t2处理“movie”输入 x2x2​ 进入正向LSTM单元2。单元2接收 h→1,C→1h1​,C1​计算得到 h→2,C→2h2​,C2​。如此继续直到t7。帧F4t7处理“all”输入 x7x7​ 进入正向LSTM单元7。单元7接收 h→6,C→6h6​,C6​计算得到 h→7,C→7h7​,C7​。至此正向LSTM完成得到所有前向隐藏状态 h→1…h→7h1​…h7​。我们可以为每个时间步添加一个小的动画展示单元内部的门控计算但为了整体清晰可以先聚焦宏观流动。6.3 反向LSTM逐帧解析帧B1初始化展示反向LSTM的初始隐藏状态 h←8h8​ 和细胞状态 C←8C8​ 为零向量位于最右侧。帧B2t7处理“all”注意反向LSTM的第一个输入是 x7x7​最后一个词。输入 x7x7​ 进入反向LSTM单元7与正向单元7共用输入但独立计算。单元7接收 h←8,C←8h8​,C8​计算得到 h←7,C←7h7​,C7​。h←7h7​ 从单元7左侧输出因为反向LSTM是从右向左传递并连接一个箭头指向单元6。帧B3t6处理“at”输入 x6x6​ 进入反向LSTM单元6。单元6接收 h←7,C←7h7​,C7​计算得到 h←6,C←6h6​,C6​。继续向左。帧B4t1处理“The”输入 x1x1​ 进入反向LSTM单元1。单元1接收 h←2,C←2h2​,C2​计算得到 h←1,C←1h1​,C1​。反向LSTM完成得到所有后向隐藏状态 h←1…h←7h1​…h7​。6.4 隐藏状态的拼接与输出帧C1拼接对于每个时间步 tt我们将 h→tht​ 和 h←tht​ 并排放置然后拼接成一个长向量 htht​用不同颜色区分两个来源。例如t4时词“not”前向隐藏状态编码了“The movie was”的信息后向隐藏状态编码了“good at all”的信息因为后向是从后往前看所以当处理“not”时它已经看到了后面的词。拼接后h4h4​ 包含了整个句子的上下文。帧C2池化将所有 htht​ 取平均或最大得到一个固定长度的向量 havghavg​。动画中展示每个 htht​ 收缩为一个点然后汇聚成一个平均向量。帧C3分类havghavg​ 输入到一个全连接层经过softmax输出正面/负面的概率。由于句子“The movie was not good at all”包含否定词“not”情感应为负面模型应能正确分类。通过这一系列动画帧观众可以直观地看到双向LSTM如何利用前后文信息在每个时间步生成丰富的表示。7. 数学公式与计算细节为了严谨我们将双向LSTM的计算用数学公式完整写出。假设输入序列 X(x1,x2,…,xT)X(x1​,x2​,…,xT​)每个 xt∈Rdxt​∈Rd。前向LSTM的参数集为 θf(Wf,Uf,bf)θf​(Wf​,Uf​,bf​) 等后向LSTM的参数集为 θbθb​通常独立随机初始化。7.1 前向LSTM递推对于 t1t1 到 TTf→tσ(W→fh→t−1U→fxtb→f)i→tσ(W→ih→t−1U→ixtb→i)o→tσ(W→oh→t−1U→oxtb→o)C~→ttanh⁡(W→ch→t−1U→cxtb→c)C→tf→t⊙C→t−1i→t⊙C~→th→to→t⊙tanh⁡(C→t)f​t​it​ot​C~t​Ct​ht​​σ(Wf​ht−1​Uf​xt​bf​)σ(Wi​ht−1​Ui​xt​bi​)σ(Wo​ht−1​Uo​xt​bo​)tanh(Wc​ht−1​Uc​xt​bc​)f​t​⊙Ct−1​it​⊙C~t​ot​⊙tanh(Ct​)​其中 h→00h0​0C→00C0​0。7.2 后向LSTM递推对于 tTtT 到 11即从右向左扫描f←tσ(W←fh←t1U←fxtb←f)i←tσ(W←ih←t1U←ixtb←i)o←tσ(W←oh←t1U←oxtb←o)C~←ttanh⁡(W←ch←t1U←cxtb←c)C←tf←t⊙C←t1i←t⊙C~←th←to←t⊙tanh⁡(C←t)f​t​it​ot​C~t​Ct​ht​​σ(Wf​ht1​Uf​xt​bf​)σ(Wi​ht1​Ui​xt​bi​)σ(Wo​ht1​Uo​xt​bo​)tanh(Wc​ht1​Uc​xt​bc​)f​t​⊙Ct1​it​⊙C~t​ot​⊙tanh(Ct​)​其中 h←T10hT1​0C←T10CT1​0。7.3 输出层每个时间步的最终隐藏状态为ht[h→t;h←t]∈R2mht​[ht​;ht​]∈R2m其中 mm 是单个LSTM的隐藏状态维度。对于序列级任务如情感分类可以使用hfinal1T∑t1Ththfinal​T1​t1∑T​ht​或取最大池化、最后一个时间步但注意最后一个时间步的前向隐藏状态 h→ThT​ 和后向隐藏状态 h←1h1​ 拼接并不能代表整个序列因此通常用池化。然后通过一个softmax层y^softmax(Wshfinalbs)y^​softmax(Ws​hfinal​bs​)7.4 参数量分析双向LSTM的参数量大约是单向LSTM的两倍。单向LSTM的参数量不考虑偏置为 4×(m×md×mm)4×(m×md×mm)其中 mm 是隐藏状态维度dd 是输入维度4对应四个门。双向则有两组这样的参数因此约为 8×(m2dmm)8×(m2dmm)。加上输出层参数模型容量更大。8. 双向LSTM的训练反向传播随时间进行双向LSTM的训练仍然使用反向传播通过时间BPTT但由于有两个方向梯度需要沿着两个时间流向传播。8.1 前向传播计算损失首先按上述公式完成前向和反向LSTM的计算得到每个时间步的 htht​然后计算损失 LL例如交叉熵损失。8.2 反向传播损失 LL 对每个参数的梯度需要分别计算。对于前向LSTM梯度从最后一个时间步 TT 反向传播到 t1t1对于后向LSTM梯度从第一个时间步 11 反向传播到 tTtT因为后向LSTM的时间顺序是逆向的所以它的BPTT方向与正向相反。两个方向的梯度计算是独立的最终将梯度相加因为参数是独立的。在实现中现代深度学习框架如PyTorch、TensorFlow会自动构建计算图并执行自动微分我们只需定义好前向结构即可。8.3 梯度裁剪由于LSTM缓解了梯度消失但仍可能发生梯度爆炸通常会在训练时应用梯度裁剪将梯度的范数限制在一个阈值内。9. 应用场景实例双向LSTM因其强大的上下文建模能力被广泛应用于各类序列任务。以下是几个典型例子。9.1 自然语言处理命名实体识别NER识别文本中的人名、地名、机构名等。例如句子“Apple Inc. is located in Cupertino.”模型需要知道“Apple”后面有“Inc.”才能正确识别为机构。BiLSTM-CRF是NER的经典基线模型。词性标注为每个单词标注词性如名词、动词等。上下文对确定词性至关重要比如“book”可以是名词或动词。机器翻译在编码器-解码器框架中编码器常用BiLSTM来生成源语言的上下文向量解码器用单向LSTM生成目标语言。情感分析如前所述捕捉否定词和情感词之间的关系。阅读理解与问答理解问题和篇章的上下文。9.2 时间序列预测股票价格预测利用过去和未来的价格信息如果是在训练时使用未来信息需要注意数据泄露实际预测时不能使用未来数据但BiLSTM在训练时可以利用未来信息帮助学习更好的表示测试时只能使用单向或经过调整的版本。传感器数据分析例如人体活动识别一段加速度信号的前后上下文有助于判断当前动作。9.3 语音识别语音帧的识别通常依赖前后音素BiLSTM被广泛用于声学模型结合CTC连接时序分类进行端到端识别。9.4 生物信息学蛋白质二级结构预测、基因序列分析等序列中的每个位置都需要左右上下文。10. 代码实现PyTorch版下面我们给出一个用PyTorch实现双向LSTM的简单示例用于情感分类任务。代码包含注释帮助理解每一步。pythonimport torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader, Dataset # 假设我们已经有了一个文本数据集词汇表大小 vocab_size嵌入维度 embed_dim # 隐藏层维度 hidden_dim输出类别数 num_classes class BiLSTMClassifier(nn.Module): def __init__(self, vocab_size, embed_dim, hidden_dim, num_classes, num_layers1, dropout0.5): super(BiLSTMClassifier, self).__init__() self.embedding nn.Embedding(vocab_size, embed_dim) # 双向LSTM self.lstm nn.LSTM(embed_dim, hidden_dim, num_layers, batch_firstTrue, bidirectionalTrue, dropoutdropout) # 由于双向LSTM输出的隐藏状态维度是 hidden_dim * 2 self.fc nn.Linear(hidden_dim * 2, num_classes) self.dropout nn.Dropout(dropout) def forward(self, x): # x shape: (batch_size, seq_len) embedded self.embedding(x) # (batch_size, seq_len, embed_dim) # LSTM 输出 lstm_out, (hidden, cell) self.lstm(embedded) # lstm_out: (batch, seq_len, hidden_dim*2) # 对序列维度取平均池化 avg_pool torch.mean(lstm_out, dim1) # (batch_size, hidden_dim*2) # 也可以取最大池化或使用最后一个时间步 # max_pool, _ torch.max(lstm_out, dim1) out self.dropout(avg_pool) logits self.fc(out) # (batch_size, num_classes) return logits # 超参数设置 vocab_size 10000 embed_dim 100 hidden_dim 128 num_classes 2 num_layers 2 dropout 0.5 model BiLSTMClassifier(vocab_size, embed_dim, hidden_dim, num_classes, num_layers, dropout) # 损失函数和优化器 criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr0.001) # 训练循环省略数据加载 # for epoch in range(num_epochs): # for batch in dataloader: # inputs, labels batch # optimizer.zero_grad() # outputs model(inputs) # loss criterion(outputs, labels) # loss.backward() # optimizer.step()说明batch_firstTrue使得输入张量的形状为 (batch, seq_len, feature)。bidirectionalTrue自动创建双向LSTM输出lstm_out的最后一个维度是hidden_dim * 2。池化方式这里用了平均池化也可改用最大池化或使用hidden状态但hidden是最后一层的隐藏状态对于多层双向hidden的形状为(num_layers * 2, batch, hidden_dim)需要处理。注意dropout的使用在多层LSTM中dropout会应用于除最后一层外的各层输出我们还在全连接前加了一个dropout。扩展如果想要获取每个时间步的输出如序列标注任务可以直接使用lstm_out并通过一个线性层映射到标签空间。11. 变体与改进多层、注意力、预训练11.1 堆叠双向LSTM将多个双向LSTM层堆叠起来可以提取更高级别的特征。例如第一层捕捉局部上下文第二层捕捉更抽象的上下文。堆叠时上一层的输出每个时间步的拼接向量作为下一层的输入。在PyTorch中只需设置num_layers 1即可。11.2 结合注意力机制在序列到序列任务或分类任务中注意力机制可以让模型关注输入序列的不同部分。例如在机器翻译中解码器在每个时间步对编码器的所有隐藏状态计算注意力权重加权求和得到上下文向量。对于双向LSTM注意力通常作用于所有时间步的拼接隐藏状态。自注意力Self-Attention也可以用在BiLSTM之上让序列自身计算注意力进一步捕捉长距离依赖。11.3 与CRF结合对于序列标注任务如NERBiLSTM上层通常连接一个条件随机场CRF。CRF能够考虑标签之间的转移关系输出全局最优的标签序列。BiLSTM提供每个位置的标签分数CRF建模标签间的约束。11.4 预训练语言模型近年来预训练模型如BERT、GPT等基于Transformer架构在许多任务上超越了BiLSTM。但BiLSTM仍然有其价值参数量较少、训练速度快、在小数据集上表现良好、易于部署。而且可以将预训练的词向量如GloVe、FastText或上下文嵌入ELMo作为BiLSTM的输入提升性能。ELMo本身就是一个双向LSTM语言模型。11.5 门控循环单元GRUGRU是LSTM的简化变体将遗忘门和输入门合并为更新门并合并细胞状态和隐藏状态。双向GRU同理计算量更小效果与LSTM相近。