Skip to main content

基于LSTM的文本分类任务

想象一下你在读一篇很长的小说,或者听一个人讲一个复杂的故事。

  • 普通人 (简单 RNN 的问题):

    • 你可能会记住故事刚开始的情节,但随着故事越来越长,你很容易忘记几百页之前提到的某个重要线索或人物。你的“短期记忆”是有限的。
    • 这就是简单循环神经网络 (RNN) 的问题:它们在处理长序列时,容易出现梯度消失或梯度爆炸,导致很难捕捉到序列中相隔很远的依赖关系(即“长期依赖”)。
  • 记忆超群的人 (LSTM 的能力):

    • 现在想象一个记忆力超群的人。他不仅能记住当前听到的内容,还能有选择地记住很久以前的关键信息,并且在需要的时候把这些信息调出来用。他还能判断哪些信息现在不重要了,可以暂时忘掉,以免大脑混乱。
    • LSTM (Long Short-Term Memory,长短期记忆网络) 就扮演了这个“记忆超群的人”的角色。 它是一种特殊的循环神经网络,专门设计用来解决简单 RNN 的长期依赖问题。

LSTM 的核心秘诀:“门控机制” (Gating Mechanism)

LSTM 之所以能“记忆超群”,是因为它内部设计了几个巧妙的“门”(Gates)。这些门就像信息流的控制开关,决定了哪些信息应该被保留、哪些应该被遗忘、哪些应该被输出。

把 LSTM 的一个单元想象成一个有“记忆细胞 (Cell State)”的房间,这个细胞负责存储长期记忆。然后有三个主要的“门卫”来管理进出这个房间和对外输出的信息:

  1. 遗忘门 (Forget Gate):

    • 作用: 决定从“记忆细胞”中丢弃哪些旧信息。
    • 例子: 当你读到一个新的章节,涉及到新的主角时,遗忘门可能会决定“前一个章节的配角信息现在不那么重要了,可以适当忘记一些”,以便为新信息腾出空间。
    • 它会查看当前的输入和前一个时刻的短期记忆,然后输出一个介于0和1之间的值,告诉记忆细胞的每个部分应该保留多少(1代表完全保留,0代表完全忘记)。
  2. 输入门 (Input Gate):

    • 作用: 决定哪些新的信息要被存储到“记忆细胞”中。
    • 例子: 当你读到一个新的关键线索时,输入门会判断“这个线索很重要,需要记下来”。
    • 它包含两部分:
      • 一部分决定哪些值需要更新(类似于遗忘门,输出0-1的值)。
      • 另一部分创建一个候选值向量,准备加入到细胞状态中。
    • 两者结合,决定了记忆细胞中哪些部分需要更新,以及用什么新值来更新。
  3. 输出门 (Output Gate):

    • 作用: 决定从“记忆细胞”中提取哪些信息作为当前时刻的输出(也作为下一个时刻的短期记忆)。
    • 例子: 当你需要根据已知信息做一个判断或回答一个问题时,输出门会从你的长期记忆中挑选相关的部分,并结合当前情况形成答案。
    • 它会根据当前的输入、前一个时刻的短期记忆和当前的细胞状态,决定细胞状态的哪些部分可以输出。

简单来说,LSTM 就是通过这三个门,智能地控制信息在“记忆细胞”中的流入、流出和保留,从而能够有效地捕捉和利用序列中的长期依赖关系。


现在我们来看你提供的 PyTorch 代码中的 nn.LSTM 参数:

self.lstm = nn.LSTM(embedding_dim,         # input_size
                    hidden_dim,            # hidden_size
                    num_layers=n_layers,   # num_layers
                    bidirectional=bidirectional, # bidirectional
                    dropout=dropout_prob if n_layers > 1 else 0, # dropout
                    batch_first=True)      # batch_first
  • embedding_dim (对应 input_size):

    • 作用: 告诉 LSTM 层,在序列的每一个时间步,输入进来的“词”或“符号”是用多少维的向量来表示的。
    • 例子: 如果你把单词“猫”转换成了一个 100 维的向量(词嵌入),那么 embedding_dim 就是 100。LSTM 在每个时间点都会接收一个这样维度的向量。
  • hidden_dim (对应 hidden_size):

    • 可以理解为LSTM层中LSTM神经元的个数
    • 作用: 定义 LSTM 内部“短期记忆”(隐藏状态,Hidden State)和“长期记忆”(细胞状态,Cell State)的维度大小。这个维度也决定了 LSTM 在每个时间步输出的特征向量的维度(如果是单向 LSTM 的话)。
    • 例子: 如果 hidden_dim 是 256,那么 LSTM 在每个时间步的“思考结果”(隐藏状态)就是一个 256 维的向量。这个“思考结果”会传递给下一个时间步,也会作为当前时间步的输出。
    • 重要性: hidden_dim 的大小影响了模型的“记忆容量”和“表达能力”。太小可能记不住足够的信息,太大则可能导致参数过多,容易过拟合,计算也更慢。
  • num_layers=n_layers

    • 可以理解为LSTM层个数
    • 作用: 指定要堆叠多少层 LSTM。就像搭积木一样,你可以把多个 LSTM 层叠在一起。
    • 例子: 如果 num_layers=2,意味着序列数据会先经过第一个 LSTM 层处理,第一个 LSTM 层的输出(每个时间步的隐藏状态)会作为第二个 LSTM 层的输入,再进行处理。
    • 好处: 更深的 LSTM (多层) 可以学习到更复杂、更抽象的序列模式。第一层可能学习一些局部的、底层的模式,更高层则在这些模式的基础上学习更全局、更高级的模式。
    • 注意: 层数越多,参数也越多,训练也越困难。
  • bidirectional=bidirectional (通常是一个布尔值 TrueFalse):

    • 作用: 决定是否使用双向 LSTM (BiLSTM)
    • 单向 LSTM (bidirectional=False): 像我们正常阅读一样,从序列的开头按顺序处理到结尾(从左到右)。它在某个时间点的输出只依赖于之前的信息。
    • 双向 LSTM (bidirectional=True): 它包含两个独立的 LSTM:一个从前向后处理序列,另一个从后向前处理序列。然后,在每个时间点,这两个 LSTM 的隐藏状态会被拼接 (concatenate) 起来(或者用其他方式如相加、平均等结合)作为最终的输出。
    • 例子: 理解一句话“我今天开心”。
      • 单向 LSTM 读到“不”的时候,可能还不知道后面的“开心”是什么。
      • 双向 LSTM 的前向部分处理到“不”,后向部分则已经处理了“开心”。结合起来,模型能更好地理解“不”在这里的否定意义。
    • 好处: 双向 LSTM 能够同时利用过去和未来的上下文信息,对于很多 NLP 任务(如情感分析、命名实体识别)通常能带来更好的性能。
    • 注意: 如果是双向的,那么在每个时间步输出的隐藏状态维度会是 hidden_dim * 2(因为是两个方向的隐藏状态拼接)。
  • dropout=dropout_prob if n_layers > 1 else 0

    • 作用:多层 LSTM 中,在除了最后一层之外的每一层 LSTM 的输出上应用 Dropout 正则化。Dropout 会在训练时随机地将一些神经元的输出置为零,以防止过拟合。
    • 例子: 如果 num_layers=3dropout_prob=0.5,那么第一层 LSTM 的输出在进入第二层 LSTM 之前,会经过一个 Dropout 层(丢弃概率为0.5);第二层 LSTM 的输出在进入第三层 LSTM 之前,也会经过一个 Dropout 层。最后一层(第三层)的输出则不加 Dropout。
    • 条件 if n_layers > 1 else 0 的含义: 这段代码的意思是,只有当 LSTM 的层数大于 1 时,才应用 dropout_prob 指定的 dropout 率;如果只有一层 LSTM,则不应用 dropout(dropout 率为0)。这是因为 dropout 通常用于层与层之间,单层 LSTM 内部没有“中间层输出”可以应用 dropout。
    • 好处: 增强模型的泛化能力,减少对训练数据的过拟合。
  • batch_first=True

    • 作用: 这是一个非常重要的参数,它定义了输入和输出张量的维度顺序。
    • batch_first=True (常用): 输入张量的形状是 (batch_size, seq_len, input_size),输出张量的形状是 (batch_size, seq_len, hidden_size * num_directions)。也就是说,批次大小是第一个维度。
    • batch_first=False (默认值): 输入张量的形状是 (seq_len, batch_size, input_size),输出张量的形状是 (seq_len, batch_size, hidden_size * num_directions)。也就是说,序列长度是第一个维度。
    • 为什么重要: 你在准备输入数据时,需要确保数据的维度顺序与这里设置的一致。大多数情况下,我们习惯于将 batch_size 放在第一个维度,所以 batch_first=True 是一个方便的选择。

总结一下 LSTM 的工作流程(结合参数):

  1. 输入: 一批序列数据,每个序列由多个时间步组成,每个时间步是一个 embedding_dim 维的向量。由于 batch_first=True,输入形状是 (batch_size, seq_len, embedding_dim)
  2. 处理:
    • 数据流经 n_layers 个堆叠的 LSTM 层。
    • 每个 LSTM 单元内部通过遗忘门、输入门、输出门来更新其细胞状态(长期记忆)和隐藏状态(短期记忆)。hidden_dim 定义了这些状态的维度。
    • 如果 bidirectional=True,则每个堆叠层都有一个前向 LSTM 和一个后向 LSTM 并行工作,它们的隐藏状态会拼接。
    • 如果在多层 LSTM 中且 dropout_prob > 0,则在非最后一层的 LSTM 输出上会应用 dropout。
  3. 输出:
    • output 张量: 包含最后一层 LSTM 在每个时间步的隐藏状态。形状是 (batch_size, seq_len, hidden_dim * num_directions) (其中 num_directions 是 1(单向)或 2(双向))。这个输出通常用于需要序列中每个位置信息的任务(如序列标注)。
    • (h_n, c_n) 元组 (隐藏状态和细胞状态):
      • h_n: 包含序列中最后一个时间步的所有堆叠层的隐藏状态。形状是 (num_layers * num_directions, batch_size, hidden_dim)
      • c_n: 包含序列中最后一个时间步的所有堆叠层的细胞状态。形状是 (num_layers * num_directions, batch_size, hidden_dim)
      • h_n (或者 h_n 的一部分) 经常被用作整个序列的编码表示,用于后续的分类、回归等任务。