资讯详情

循环神经网络三件套:RNN、LSTM与BiLSTM原理与选型指南

📅 2026/10/21 18:28:37 | 华诺云谱 👁 阅读
循环神经网络三件套:RNN、LSTM与BiLSTM原理与选型指南
第一次在实体识别任务里把循环神经网络换成双向结构效果直接涨了好几个百分点。当时模型经常把“李明在北京出差”这种人名片段切错明明后文已经给出了线索网络却只盯着前面看。后来把LSTM换成BiLSTM这类错误明显少了大半。也是从那次之后我意识到循环神经网络的这三件套——RNN、LSTM、BiLSTM——本质上是一步一步把“记忆”这件事做得更精细。这三种结构是序列建模里绕不开的基础。很多刚接触深度学习的同学第一反应是图像用卷积、文本语音用循环网络。但实际做项目时你会发现循环网络不只是“处理序列的工具”它背后真正的问题是模型如何利用已经看过的信息去理解当前这一时刻的内容。RNN提出了循环共享的思想LSTM解决了长距离记忆的传递BiLSTM则把未来信息也借了过来。这篇文章适合正在入门自然语言处理、时序预测或者想弄清楚这三种结构到底差在哪的读者。我会从算法思路本身出发结合平时踩过的坑和实际选型经验把这三个模型讲明白。1. 为什么序列建模绕不开循环结构1.1 普通网络处理不了序列的三个硬伤先说一个很直观的问题全连接网络根本没法处理变长序列。假设你要对一条用户评论做情感判断句子有长有短。全连接网络的输入维度是固定的你只能把所有词向量拼成一个大向量或者干脆取平均。取平均的问题特别明显“猫追狗”和“狗追猫”这两个意思完全相反的句子词向量取平均之后几乎一样。顺序信息在平均这一步就彻底丢了。拼长向量看起来保留了顺序但实际上是把序列强行压进一个固定尺寸的容器里。句子长一点就放不下短一点又浪费大量位置。更麻烦的是全连接网络里每个位置都有自己独立的一组权重词出现在第一个位置和第三个位置用的是完全不同的参数。这意味着网络没法把“在开头学到的模式”迁移到“在中间出现的情况”上。卷积网络能缓解一部分局部顺序问题。一维卷积可以看一个固定窗口内的词比如窗口为3就能同时看到“猫追狗”。但窗口大小一固定想看到跨句子级别的长期依赖就得堆很多层卷积感受野才有机会覆盖到远处。这个成本非常高而且远距离信息经过多层卷积后衰减得很厉害。所以核心矛盾是三个输入长度不定、参数不能在时间维上共享、长距离顺序关系难捕捉。循环神经网络的出现就是冲着解决这三个问题去的。1.2 循环连接到底在做什么循环神经网络的核心是一个递归调用的单元。在当前时间步 t网络的隐藏状态 h_t 由当前输入 x_t 和前一步的隐藏状态 h_{t-1} 共同决定。这个过程可以写成h_t tanh(W_hh · h_{t-1} W_xh · x_t b_h)这个公式是RNN家族的“地基”后面所有的花样都是在这个递归关系上做文章。你可以把循环结构理解成在看连续剧。如果不记得前几集发生了什么只看单集很难理解人物之间的恩怨。循环网络里的隐藏状态本质上就是一个“剧情总结”每来一幕新剧情它就把之前的总结和当前这一幕的内容合并生成一份新的总结。这个总结会一直往下传信息也就不停在时间维度上流动。另一个关键设计是权重共享。同一个单元在每一个时间步都在被重复使用不管序列长度是10还是1000参与计算的参数总量是不变的只有同一个矩阵 W_hh 和 W_xh 在反复做运算。这带来两个好处参数量不会跟着序列长度线性增长同时模型学会的是一种“时间上的规律”而不是某个固定位置上的特征。对变长输入来说循环结构几乎是天然适配的。它不需要把句子pad到固定长度只需要按时间步逐个喂进去最终无论序列多长都能产出对应的输出。1.3 一次前向计算从输入到输出的完整路径把上面的公式展开来看一次完整的前向计算。假设输入序列是 x_1, x_2, ..., x_T每个 x_t 是一个维度为 d 的向量。初始时刻 t0 时隐藏状态 h_0 通常初始化为全零向量。然后对 t1 到 T 循环执行两步操作第一步更新隐藏状态h_t tanh(W_hh · h_{t-1} W_xh · x_t b_h)。第二步根据隐藏状态产生输出y_t W_hy · h_t b_y。这里 W_hh 是隐藏状态到隐藏状态的转移矩阵形状是 hidden_size × hidden_sizeW_xh 是输入到隐藏状态的矩阵形状是 hidden_size × input_sizeW_hy 是隐藏状态到输出的矩阵形状由输出维度决定。在分类任务里不一定要每个时间步都产生输出。比如情感分析你只需要最后一个隐藏状态 h_T把这个向量丢到一个全连接层加softmax里即可。但比如语言模型每个时间步都需要预测下一个词那就得每个时间步都输出一个概率分布。用维度来解释可能更直观。假设一个batch里有 batch_size 句话每句话长度是 seq_len每个词嵌入成 input_size 维向量那么输入张量形状是 batch_size × seq_len × input_size。循环神经网络本质上是沿着seq_len这一个维度做逐步运算每一步取出一个时间步的切片 x_t形状是 batch_size × input_size乘上输入矩阵加上上一个时间步的隐藏状态 h_{t-1}得到当前步的 h_t形状是 batch_size × hidden_size。整个过程中 seq_len 这个维度被“逐步消费”掉而隐藏状态就是网络携带的记忆容器。这个流程看起来简单但真正训练起来的时候麻烦就来了。下一节具体看RNN是怎么训练的以及它卡在什么地方。2. RNN最简单的循环思想2.1 权重共享和时间步展开RNN的“循环”其实很朴素在每一个时间步重复使用同一个网络单元。但很多初学者会困惑既然只有一个单元那网络“深”在哪儿把时间维度展开就清楚了。把 h_1 到 h_T 的计算全部画出来这其实是一个有 T 层的网络结构。第 t 层接收两个输入一个是 x_t另一个是上一层的输出 h_{t-1}。所以RNN在结构上等价于一个深度为 T 的前馈网络只是网络在不同层之间共享同一套权重。这里能引出一个经验点RNN参数量小是真的小一个简单的RNN层可能只有几百个参数却能处理任意长度的序列。这在资源受限的场景里很有吸引力。不过代价就是它的表达能力有限因为循环权重是要被所有时间步共享的相当于它假设“时间规律是平稳的”。也就是过去学到的转移规律在未来依然适用。绝大多数序列数据符合这个假设比如语言的语法规则不会因为你在第100个词还是第3个词而改变。但如果处理的是有明显分布漂移的数据比如金融时序数据这个假设就得打问号了。实际做项目时我喜欢先把RNN当做一个极简基线。不是因为RNN效果多好而是因为它结构足够简单跑起来足够快。如果RNN都能达到勉强可用的效果说明任务的时序依赖不难后续可以放心上LSTM或者Transformer如果RNN完全学不动那就要先怀疑数据或特征有没有把序列信息表达好。2.2 训练过程BPTT把梯度送回过去RNN的训练用的是时间反向传播缩写为BPTT全称 Backpropagation Through Time。它并不是另一个新算法本质上就是反向传播在时间维度上的推广。前向计算时网络会按时间顺序算出一整套隐藏状态把这些状态全部缓存下来。反向传播时从最后一个时间步开始计算损失对当前时间步输出的梯度然后沿着 h_T → h_{T-1} → ... → h_1 这条路径把梯度一步步往回流。展开后网络的深度就是序列长度所以反向传播回传的路径长度也等于序列长度。这里有个细节很容易忽略BPTT计算的损失是每个时间步损失的总和。分类任务可能只有一个最终损失但语言模型或者序列标注任务每个时间步都会产生损失这些损失会同时影响梯度。反向传播时要把来自后续所有时间步的梯度叠加起来。公式上对权重 W_hh 的梯度可以写成各个时间步贡献的累加∂L/∂W_hh Σ_t (∂L_t / ∂W_hh)每个时间步的贡献又依赖前面所有时间步的传递路径所以同一个权重矩阵沿着一条很长的链反复被求导。这意味着什么呢意味着RNN虽然参数量小训练时的计算图却特别长。序列长度一旦到几百甚至上千反向传播的内存开销和计算开销都很大。所以实际工程里很少有人真的在超长序列上从头训一个RNN通常会用截断BPTT只把梯度回传到最近的一两个窗口步长内。2.3 RNN的短板长距离依赖学不动如果只看前向传播和BPTTRNN似乎已经足够优雅。但真正训练过一次你就会发现序列一长效果会崩。问题出在梯度回传路径上的连乘结构。h_t 对 h_{t-1} 的导数里包含 tanh 的导数项和 W_hh 本身。tanh 的导数最大只有1当输入落在饱和区时导数直接趋近于0。假设每一步的梯度都乘以一个小于1的数经过 T 步之后梯度是原来的 0.9 的 T 次方。序列长度50时大约是 0.9^50 ≈ 0.005梯度基本衰减没了序列长度100时直接趋近于0。这就是经典的梯度消失问题。梯度消失的直观后果是网络根本没有办法把“远处的信息”学进参数里。注意力机制虽然能综合当前信息但梯度传不回来更新不了前面时间步对应的模型权重所以模型只能记住相邻几个时间步内的依赖关系。另一种情况是梯度爆炸如果 W_hh 的主特征值大于1连乘结果会指数增长梯度直接溢出变成NaN。工程上梯度爆炸比较好处理梯度裁剪抓一下就行。但梯度消失基本没法靠后处理解决必须从结构层面改变传播路径。用传话游戏类比最贴切一个人对下一个人低声说一句话每传一次都会折损一点或者添油加醋一点传了十几次之后最后那个人听到的基本是另一个版本了。经典RNN就是这么个状态长距离依赖它真的学不动。这也是LSTM被设计出来的根本动机。3. LSTM用门控管理记忆3.1 从单条线性传递到细胞状态LSTM全称是长短期记忆网络思路很直接给RNN加一条贯穿时间轴的“传送带”。这条传送带叫细胞状态通常记为 c_t。在经典RNN里隐藏状态每个时间步都要经过 tanh 变换信息被压缩在 [-1, 1] 区间一旦更新就被扰动很难原样不动地传下去。而细胞状态这条传送带的与众不同之处在于它允许信息以接近线性的方式流过整个时间轴中间不需要经过非线性激活函数。这就好比你在仓库里修了一条主传送带平时货物不管要不要处理都会顺着传送带流过只有特定位置才有分拣员决定是否把某件货物拿走或者放上新货。这三个分拣动作对应LSTM里的三个门控遗忘门决定从细胞状态里丢弃多少旧信息输入门决定把多少新信息写入细胞状态输出门决定这一时刻的隐藏状态对外暴露多少记忆。整套机制就是围绕着“传送带上的信息管理”来设计的。这个设计带来的第一个直观变化是模型有了独立的“记忆存储”和“外部输出”的分离。隐藏状态 h_t 是给当前任务看的“工作记忆”细胞状态 c_t 是留给长期保存的“长期记忆”。工作记忆可以被当前输入覆盖长期记忆则经过精心挑选才更新。3.2 三个门控的职责分工LSTM的计算过程比RNN复杂不少但每个部分职责都很明确。先看公式再逐个解释。遗忘门f_t σ(W_f · [h_{t-1}, x_t] b_f)输入门i_t σ(W_i · [h_{t-1}, x_t] b_i)候选记忆c̃_t tanh(W_c · [h_{t-1}, x_t] b_c)细胞状态更新c_t f_t ⊙ c_{t-1} i_t ⊙ c̃_t输出门o_t σ(W_o · [h_{t-1}, x_t] b_o)隐藏状态输出h_t o_t ⊙ tanh(c_t)σ 是sigmoid函数输出在0到1之间表示“开门程度”0代表完全关闭、1代表完全打开。⊙ 是逐元素乘法。遗忘门 f_t 看的是当前输入 x_t 和上一个隐藏状态 h_{t-1}输出一个0到1的向量分别对应细胞状态每个维度的遗忘程度。它告诉网络旧的记忆里哪些部分已经过期可以清理掉了。比如处理一篇新闻报道时前一个段落讲的是天气现在开始讲交通网络可以通过遗忘门把关于天气的记忆系数调低。输入门 i_t 不对记忆做修改它只是“审核员”。真正的内容是由候选记忆 c̃_t 提供的c̃_t 通过 tanh 生成了一个新的候选向量范围在-1到1之间。输入门决定这个候选向量有多少会真的写入细胞状态。这就像一个部门提交了一份提案得经过审批才能落地。输出门 o_t 的职责有点特殊。它不直接影响细胞状态而是决定当前细胞状态 c_t 里哪些信息可以被暴露给外界。h_t 既要传给下一时间步又要用来生成当前输出。如果所有记忆全部暴露出来可能反而让当前任务被无关的历史信息干扰。输出门就是这个筛子用来控制对外可见的记忆内容。细胞状态更新的公式 c_t f_t ⊙ c_{t-1} i_t ⊙ c̃_t 是整个LSTM最核心的地方先按遗忘门清理旧记忆再按输入门写入新记忆。整个过程是线性叠加而不是重写。3.3 LSTM为什么能对抗梯度消失LSTM比RNN能记住更长距离的信息关键在于细胞状态的更新是加法操作。在回传梯度时c_t 对 c_{t-1} 的导数是 f_t再加上其他路径的贡献。理想的传播路径下遗忘门接近1那么细胞状态在时间维上几乎是无损的直线梯度可以在这条路径上畅通无阻地往回传。与经典RNN里每一步都乘一个 tanh 导数和矩阵权重不同LSTM的这条主路径没有强制性的非线性压缩。这跟残差网络里“跳跃连接”的思路很像。残差网络通过把上一层的输出直接加到这一层的输出上让梯度有一条高速公路可以绕过非线性变换直接回流。LSTM的细胞状态就是这条高速公路。它就是给梯度提供了一个不受连乘影响的旁路。但要注意LSTM并没有完全消除梯度消失。如果遗忘门长期打开梯度确实通畅但如果遗忘门训练到了接近0的状态代表网络主动选择遗忘之前的信息这一条路径上的梯度同样会被截断。这种截断是任务需要的结果不是纯粹的缺陷。理解这一点很重要LSTM不是“永远记着一切”而是“学会决定要记什么”。当它选择遗忘某些旧记忆时相关位置的梯度路径自然就断了。实际项目中还会有另一个体会LSTM的参数量大约是RNN的四倍训练速度明显变慢。如果你只想做一个快速基线GRU是个不错的折中方案。GRU把三个门缩减成更新门和重置门两个门参数更少在很多中等规模任务上跟LSTM效果差不了多少。但本文以LSTM为主线因为理解LSTM的门控逻辑之后GRU几乎是顺带就能看懂的东西。4. BiLSTM把后文也纳入视野4.1 单向模型的“左侧依赖”局限很多新手刚接触时默认循环网络是“从前往后”看的。这个习惯继承自阅读顺序。但读句子理解语义时你往往需要往前翻一翻才能确定当前词的意思。举一个实体识别里非常典型的例子“李明的报告已经提交他在会上做了说明”。如果模型只看“他”之前的文本它知道“他”大概率是指代某个人。但具体指代李明还是另一个没说名字的人取决于后文“在会上的说明内容”。后文信息就是解锁指代的关键。单向LSTM在这个任务上天生吃亏它只能利用当前词左侧上下文右侧信息必须靠模型预测出来但那相当难。情感分析里也有类似案例。“虽然这家店环境很好但服务态度让人崩溃”。只看前半句“环境很好”模型会觉得是正面的转折词“但”之后内容才是真正要传达的态度。单靠前文运行到最后一个时间步时模型很难准确判断整句的立场因为最后一个词只是结尾并不携带完整的全局信息。所以有一类任务天然“需要看后文”。实体识别、关系抽取、句子分类、阅读理解这些任务的共同特征是当前位置的理解依赖整个句子甚至整个文档的上下文而不仅仅是从左到右的过去信息。4.2 双向结构两个方向同时编码BiLSTM的思路朴素到有点讨巧既然单向只能看左边那就再跑一个方向从右往左看把两个方向的信息拼起来。具体做法是准备两套独立的LSTM参数。前向LSTM按 x_1, x_2, ..., x_T 的顺序逐个读入得到一组前向隐藏状态 h_1^f, h_2^f, ..., h_T^f。后向LSTM按逆序 x_T, x_{T-1}, ..., x_1 逐个读入得到另一组后向隐藏状态 h_1^b, h_2^b, ..., h_T^b。然后对每一个时间步 t把前向和后向两种状态拼接成一个向量h_t [h_t^f ; h_t^b]这个拼接后的向量就是 BiLSTM 在位置 t 的表示。它的含义是当前词的信息加上来自左侧的上下文信息再加上来自右侧的上下文信息三者共同编码在一起。还是用“李明在北京出差”这个例子。前向LSTM读到“李”的时候当前语境里还不知道后面是个“明”字但后向LSTM从右往左读的时候先看到“明”再看到“李”两个方向的信息一汇合模型就知道“李”和“明”是同一个实体片段里的字。这个能力不是单向模型通过调参能模仿出来的它真的是结构上带来了右侧信息。所以在实际使用中第 t 个位置的表示质量会明显高于单向LSTM的输出。这也是为什么在大量自然语言处理任务上BiLSTM成为标配。毫不夸张地说预训练语言模型出现之前BiLSTM加CRF几乎就是序列标注任务的标准答案。4.3 双向的代价与现实取舍双向结构效果更好代价也实打实地摆在面前。参数量直接翻倍。前向一套 LSTM后向一套 LSTM两套完全不共享参数。内存占用和训练时间都大约变成两倍。在长序列上这个开销尤其明显。更大的限制是推理模式的改变。单向LSTM可以做流式处理来一个输入时间步更新一次隐藏状态立刻输出当前结果。BiLSTM做不到这一点因为后向那一半需要未来信息你必须在拿到完整序列之后才能运行后向LSTM。这也是为什么语音识别里实时解码的模型很少用BiLSTM作为主结构延迟不可接受。等你把整句话都收完再解码用户已经等不住了。另外双向结构不天然适合某些增量的预测场景比如实时键盘输入预测、逐字手写识别这类任务是“只见过过去、没见过未来”的结构上就不该使用双向编码。实用建议是先想清楚任务到底是离线还是在线。离线自然语言处理任务基本可以放心上BiLSTM。在线流式任务只能单向结构加额外机制或者用一些延迟可控的双向变体。很多坚持用单向LSTM的团队不是不理解双向的好处而是实时性约束把他们卡死了。5. 选型参考RNN、LSTM、BiLSTM怎么选5.1 按任务特性选结构很多初学者会到处问“这个任务应该用哪个模型”。与其记结论不如按任务特性来判断。判断依据有三条序列长度通常是多少、当前位置的理解是否依赖后文、推理时能不能等完整序列到齐。根据这三条我通常会这样选结构适用场景优势明显短板RNN极短序列的快速基线、实时流式场景、计算资源受限参数极少、实现简单、可在线推理长距离依赖记不住LSTM时序预测、常规序列分类、语言建模、在线推理需求能记住较长依赖、训练稳定、可流式只能看到左侧上下文BiLSTM离线自然语言处理、实体识别、关系抽取、句子级分类左右上下文都能利用、表示质量高参数翻倍、无法流式推理注意这个表不是死的。比如RNN其实也有双向版本LSTM也可以做成双向。但选型时抓住“数据依赖是不是长距离的”和“要不要用后文”这两个核心矛盾就够了。我自己还会补充一个习惯先用BiLSTM跑一版看效果上界再用单向LSTM对比。如果单向和双向差距很小说明这个任务可能更依赖全局统计而非顺序关系后续可以直接考虑换成并行化更高的结构。如果双向优势明显说明左右上下文都很关键这个任务值得继续在序列模型上投入。5.2 实操中常见的坑讲几个我在实际工程里踩过的坑都是文档里不会写的事。第一个是mask问题。序列被 padding 成固定长度后模型把填充位置当成了真实输入。这会让最后的隐藏状态包含无效信息。最简单的方法是记录每个样本的真实长度在取 h_T 时通过索引取出真实最后一步的隐藏状态而不是天真地使用序列最后一个位置的输出。这个细节在Transformer里有专门处理但在LSTM年代很多人都会忽略。第二个坑是初始化遗忘门的偏置。默认把遗忘门偏置初始化为0模型一开始遗忘门输出是0.5相当于先丢掉一半历史。常见做法是初始化为1或2让遗忘门在训练初期接近打开状态保留足够的信息量。这个设置在我做长文本分类时帮助非常明显。第三个是序列长度差异过大的batch。假设一个batch里最长句子100个词最短的只有5个词大量空白padding位置浪费了计算资源还会影响模型稳定性。实践中按长度排序并做长度分组也叫bucket会让训练快很多梯度也更加稳。这对LSTM尤其重要因为LSTM的计算是严格时间顺序的没法像Transformer那样并行。第四个是别一上来就堆多层。经常看到有人在LSTM外面叠三层甚至更多。实际经验是这类序列模型加层数的收益递减非常快但训练时间掉得很快。先跑通单层双向把数据、特征和损失函数调好再考虑加深。第五个是关于dropout的位置。LSTM的dropout如果加在循环连接上训练时会让隐藏状态抖动得很厉害。常规做法是在每个时间步的输入和输出部分加dropout循环连接内部保持稳定。这个细节的实现方式取决于框架但思路是一样的给模型加正则但不要打断时间维上的信息流动。5.3 和注意力机制结合的习惯做法近几年做自然语言处理BiLSTM很少直接作为最终输出层使用它更多承担的是“编码器”的角色。习惯做法是BiLSTM编码完之后接一个注意力层。为什么因为BiLSTM最后时间步的输出只代表该位置看到的双向上下文不代表整个句子的全局主题。文档级或者句子级的分类任务里真正重要的情感词可能出现在第3个词、第17个词、第8个词这几个位置简单把最后一个向量接全连接再丢softmax等于假设“末尾信息能代表全句”。这个假设在许多任务上并不成立。注意力层做的事情是在全部时间步的表示上做一个加权求和权重由每个位置跟当前任务的相关程度决定。比如“虽然这家店环境很好但服务态度让人崩溃”这句话里注意力权重会集中在“虽然”“但”“崩溃”这几个关键位置加权求和之后得到的向量比直接取最后一个隐藏状态更能代表整句话的情感。所以实际项目中我常用这条路BiLSTM把序列编码成每个位置的上下文表示注意力层汇总这些表示成全局向量再接一层全连接做分类。这套组合在预训练模型出现以前是很多自然语言理解任务能够到当时最好效果的标准方案。即便现在用了Transformer类的预训练网络理解BiLSTM的编码思路和注意力如何配合依然能帮你更好地看懂更复杂模型的设计动机。写在最后的一点体会这三类模型的演进逻辑本质上是在解决同一个问题网络如何更合理地去组织和读取信息。RNN提出了“把历史信息装进隐藏状态”的思路LSTM细化了信息管理流程增加了细胞状态和门控机制BiLSTM进一步补上了后文信息的通道。我早期喜欢把BiLSTM当万能模板不管什么任务直接套。后来才发现关键不是选哪个结构而是先搞清楚数据的依赖关系到底在什么范围、需不需要后文、能不能接受延迟。这些问题答案清楚了模型结构自然就清楚了。还有一个想法是理解LSTM的门控和细胞状态并不会因为它老就过时。信息流如何被保留、被筛选、被利用这个思维框架是很多后续模型的底层出发点。如果你在学习的时候是照着这个角度去理解的那后面看注意力机制也会觉得顺理成章因为它们关心的是同一件事。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑