自然语言处理-序列到序列模型(Seq2seq)
2024-06-12 19:45:08

自然语言处理-序列到序列模型(Seq2seq)

第一章 Seq2seq

第一节 什么是Seq2Seq模型?

Seq2Seq(序列到序列)模型是一种用于处理顺序数据的机器学习模型,能够将输入序列转换为输出序列。在Seq2Seq模型出现之前,机器翻译系统主要依赖于统计方法和基于短语的方法,最流行的是基于短语的统计机器翻译(SMT)系统。然而,这些方法在处理长距离依赖关系和捕捉全局上下文方面存在局限性。

Seq2Seq模型利用神经网络,尤其是循环神经网络(RNN),解决了这些问题。谷歌在题为“使用神经网络进行序列到序列学习”的论文中首次提出了Seq2Seq模型的概念。这篇研究论文介绍的架构成为自然语言处理任务的基本框架。

Seq2Seq模型采用编码器-解码器(encoder-decoder)结构。编码器处理输入序列并将其转换为固定大小的隐藏表示,解码器则使用隐藏表示来生成输出序列。编码器-解码器结构允许模型处理不同长度的输入和输出序列,使其能够高效地处理顺序数据。Seq2Seq模型通过输入-输出对的数据集进行训练,其中输入和输出都是一系列标记。模型通过最大化在给定输入序列情况下生成正确输出序列的可能性进行训练。

神经网络架构的进步推动了更强大的Seq2Seq模型的发展,例如Transformer模型。《Attention is all you need》是一篇研究论文,首次介绍了Transformer模型。这种模型引入了注意力机制和独立的编码器与解码器堆栈,大大提高了处理语言相关任务的效率和效果。

tensorflow - Understanding Seq2Seq model - Stack Overflow

第二节 Seq2seq模型基本思想

Seq2Seq模型是一种强大的神经网络架构,用于将一个序列转换为另一个序列,广泛应用于自然语言处理(NLP)等领域。这种模型特别擅长处理输入和输出长度可能不同的任务,例如机器翻译、文本摘要和对话生成。Seq2Seq模型的核心在于其编码器-解码器结构,其中编码器将输入序列编码成固定大小的隐藏表示,解码器利用这个表示生成输出序列。

机器翻译:

神经网络机器翻译技术及应用(上)_百度神经网络翻译-CSDN博客

文本摘要:

Text Summarization Techniques(assets/1_GIVviyN9Q0cqObcy-q-juQ.png) — 概述| by Moris | NLP & Speech Recognition  Note | Medium

第三节 Seq2Seq 模型中的输入和输出

在Seq2Seq模型中,输入序列和输出序列分别记为 $X$ 和 $Y$。输入序列的第 $i$ 个元素表示为 $x_i$,输出序列的第 $j$ 个元素表示为 $y_j$。通常 $x_i$ 和 $y_j$ 是 one-hot 向量。

例如,在自然语言处理(NLP)中,one-hot 向量表示单词,其大小等于词汇量。假设输入和输出的词汇表分别为 $V^{(s)}$ 和 $V^{(t)}$,则所有元素 $x_i$ 和 $y_j$ 满足 $x_i \in \mathbb{R}^{|V^{(s)}|}$ 和 $y_i \in \mathbb{R}^{|V^{(t)}|}$。

输入序列 $X$ 和输出序列 $Y$ 可以表示为:

$$ X = (x_1, \ldots, x_I) = (x_i)_{i=1}^I $$ $$ Y = (y_1, \ldots, y_J) = (y_j)_{j=1}^J $$

其中, $I$ 和 $J$ 分别是输入序列和输出序列的长度。使用典型的NLP符号, $y_0$ 是BOS(句子开头的符号)的one-hot向量,表示句子开头的虚拟词,而 $y_{J+1}$ 是EOS(句子结尾的符号)的one-hot向量,表示句子结尾的虚拟词。

通过这种方式,Seq2Seq模型能够处理不同长度的输入和输出序列,并且通过训练数据中的输入-输出对,最大化在给定输入序列时生成正确输出序列的概率。这使得Seq2Seq模型在许多需要处理顺序数据的任务中表现出色。

第四节 条件概率符号 $P(Y|X)$

让我们来探讨条件概率 $P(Y|X)$,即在给定输入序列 $X$ 的情况下生成输出序列 $Y$ 的概率。

Seq2Seq模型的目标是对这一概率进行建模。然而,Seq2Seq模型并不是直接对 $P(Y|X)$ 进行建模,而是对 $ P(y_j|Y_{\lt j}, X) $ 进行建模,即在给定输入序列 $X$ 和前面的输出序列 $Y_{\lt j}$ 的情况下,生成第 $j$ 个输出元素 $y_j$ 的概率。

这里,$Y_{\lt j}$ 表示输出序列中的前 $j-1$ 个元素,或者 $(y_1, y_2, \ldots, y_{j-1})$。

通过这种方式,可以将模型 $P_{\theta}(Y|X)$ 表示为各个条件概率 $P_{\theta}(y_j|Y_{\lt j}, X)$ 的乘积:

$$ P_{\theta}(Y|X) = \prod_{j=1}^{J+1} P_{\theta}(y_j|Y_{\lt j}, X) $$

这里,$J$ 是输出序列 $Y$ 的长度,而 $J+1$ 表示包括了句子结尾符号(EOS)的概率。通过这种分解方式,Seq2Seq模型能够逐步生成输出序列,每一步生成一个输出元素,并且每一步都依赖于之前生成的输出和输入序列。

第五节 Seq2Seq 模型的处理步骤

Seq2Seq 模型的特点在于它由两个过程组成:

  1. 生成固定大小向量 $z$ 的过程:从输入序列 $X$ 中生成固定大小的向量 $z$。
  2. 生成输出序列的过程:从向量 $z$ 生成输出序列 $Y$。

换句话说,输出序列 $Y$ 是从向量 $z$ 生成的,概率 $P_{\theta}(y_j | Y_{\lt j} , X)$ 实际上是通过计算 $P_{\theta}(y_j | Y_{\lt j}, z)$ 得出的。

首先,我们用函数 $\Lambda$ 表示从输入序列 $X$ 生成向量 $ z $ 的过程:

$$ z = \Lambda(X) $$

函数 $\Lambda$ 可能是循环神经网络(RNN),例如 LSTM。

其次,我们用以下公式表示从向量 $z$ 生成输出序列 $Y$ 的过程:

$$ \begin{split} P_{\theta}(y_j | Y_{\lt j}, X) = \Upsilon(h_j^{(t)}, y_j) \\ h_j^{(t)} = \Psi(h_{j-1}^{(t)}, y_{j-1}) \end{split} $$

其中:

  • $\Psi$ 是生成隐藏向量 $h_j^{(t)}$ 的函数。
  • $\Upsilon$ 是计算独热向量 $y_j$ 的生成概率的函数。

当 $j = 1$ 时,$h_{j-1}^{(t)}$ 或 $h_0^{(t)}$ 是由 $\Lambda(X)$ 生成的 $z$,而 $y_{j-1}$ 或 $y_0$ 是 BOS(序列开始)的独热向量。

第六节 Seq2Seq 模型架构

eq2Seq 模型的架构可以分为五个主要角色:

  1. 编码器嵌入层
  2. 编码器循环层
  3. 解码器嵌入层
  4. 解码器循环层
  5. 解码器输出层

这些角色分别承担不同的任务,共同完成输入序列到输出序列的转换。

复制代码

编码器由两层组成:嵌入层和循环层,解码器由三层组成:嵌入层、循环层和输出层。

符号及定义:

参数 定义
H 隐藏向量的大小
D 嵌入向量的大小
$x_i$ one-hot 向量,输入句子中的第 i 个单词
$\bar x_i$ 嵌入向量,输入句子中的第 i 个单词
$E^{(s)}$ 编码器的嵌入矩阵
$h_i^{(s)}$ 编码器的第 i 个隐藏向量
$y_j$ one-hot 向量,输出句子中的第 j 个单词
$\bar y_j$ 嵌入向量,输出句子中的第 j 个单词
$E^{(t)}$ 解码器的嵌入矩阵
$h_j^{(t)}$ 解码器的第 j 个隐藏向量

Seq2Seq 模型有许多不同的变体。我们可以根据以下方面使用不同的 RNN 模型:

  1. 方向性:单向或双向
  2. 深度:单层或多层
  3. 类型:普通 RNN、长短期记忆 (LSTM) 或门控循环单元 (GRU)
  4. 附加功能:注意机制(Attention Mechanism)

6.1 编码器嵌入层

第一层为编码器嵌入层,将输入句子中的每个单词转换为嵌入向量。在处理输入句子中的第 $i$ 个单词时,该层的输入和输出如下:

  • 输入是 $x_i$ :表示第 $x_i$ 个单词的独热向量。
  • 输出是 $x_i$ :表示第 $\bar{x}_i$ 个单词的嵌入向量。

每个嵌入向量通过以下公式计算:

$$ x_i = E^{(s)} x_i $$

其中,$E^{(s)} \in \mathbb{R}^{D \times |\mathcal{V}^{(s)}|}$ 是编码器的嵌入矩阵。


计算例子:

通过一个具体的例子来解释。

假设我们有以下词汇表和嵌入矩阵:

$$ \begin{align*} \mathcal{V}^{(s)} &= \{ \text{"I"}, \text{"am"}, \text{"happy"} \} \\ E^{(s)} &= \begin{bmatrix} 0.1 & 0.2 & 0.3 \\ 0.4 & 0.5 & 0.6 \\ 0.7 & 0.8 & 0.9 \\ 0.2 & 0.3 & 0.4 \\ \end{bmatrix} \end{align*} $$

现在,我们来计算每个单词的嵌入向量。

  1. 对于单词 “I”:
$$ x_{\text{"I"}} = E^{(s)} [:, 1] = \begin{bmatrix} 0.1 \\ 0.4 \\ 0.7 \\ 0.2 \end{bmatrix} $$
  1. 对于单词 “am”:
$$ x_{\text{"am"}} = E^{(s)} [:, 2] = \begin{bmatrix} 0.2 \\ 0.5 \\ 0.8 \\ 0.3 \end{bmatrix} $$
  1. 对于单词 “happy”:
$$ x_{\text{"happy"}} = E^{(s)} [:, 3] = \begin{bmatrix} 0.3 \\ 0.6 \\ 0.9 \\ 0.4 \end{bmatrix} $$

这样,我们就得到了每个单词的嵌入向量。


6.2 编码器循环层

编码器循环层从嵌入向量生成隐藏向量。在处理第 $i$ 个嵌入向量时,该层的输入和输出如下:

  • 输入是 $x_i$:表示第 $i$ 个单词的嵌入向量。
  • 输出是 $h_i^{(s)}$:第 $i$ 个隐藏向量。

例如,当使用单层单向 RNN 时,该过程可以表示为函数 $\Psi^{(s)}$ :

$$ \begin{split} h_i^{(s)} &= \Psi^{(s)}(x_i, h_{i-1}^{(s)}) \\ &= \tanh \left( W^{(s)} \left[ \begin{array}{c} h_{i-1}^{(s)} \\ x_i \end{array} \right] + b^{(s)} \right) \end{split} $$

在这种情况下,我们使用 $\tanh$ 作为激活函数。


计算例子:

假设以下参数值:

  • 隐藏状态维度 $H = 3$
  • 嵌入向量维度 $D = 2$

并给定以下嵌入向量和先前的隐藏状态:

  • 嵌入向量 $x_i = [0.1, 0.2]$
  • 先前的隐藏状态 $h_{i-1}^{(s)} = [0.3, 0.4, 0.5]$

假设我们的权重矩阵 $W^{(s)}$ 和偏置向量 $b^{(s)}$ 如下:

$$ W^{(s)} = \begin{bmatrix} 0.1 & 0.2 & 0.3 \\ 0.4 & 0.5 & 0.6 \\ \end{bmatrix}, \quad b^{(s)} = \begin{bmatrix} 0.1 \\ 0.2 \\ 0.3 \\ \end{bmatrix} $$

现在我们来计算隐藏状态 $h_i^{(s)}$。

$$ \begin{split} h_i^{(s)} &= \tanh \left( W^{(s)} \left[ \begin{array}{c} h_{i-1}^{(s)} \\ x_i \end{array} \right] + b^{(s)} \right) \\ &= \tanh \left( \begin{bmatrix} 0.1 & 0.2 & 0.3 \\ 0.4 & 0.5 & 0.6 \\ \end{bmatrix} \begin{bmatrix} 0.3 \\ 0.4 \\ 0.5 \\ 0.1 \\ 0.2 \\ \end{bmatrix} + \begin{bmatrix} 0.1 \\ 0.2 \\ 0.3 \\ \end{bmatrix} \right) \\ &= \tanh \left( \begin{bmatrix} 0.58 \\ 1.34 \\ \end{bmatrix} + \begin{bmatrix} 0.1 \\ 0.2 \\ 0.3 \\ \end{bmatrix} \right) \\ &= \tanh \left( \begin{bmatrix} 0.68 \\ 1.54 \\ \end{bmatrix} \right) \\ &= \begin{bmatrix} 0.5920 \\ 0.9171 \\ \end{bmatrix} \end{split} $$

因此,根据给定的参数和输入,我们得到了新的隐藏状态 $h_i^{(s)} = [0.5920, 0.9171]$。


6.3 解码器嵌入层

解码器嵌入层将输出句子中的每个单词转换为嵌入向量。在处理输出句子中的第 $j$ 个词时,该层的输入和输出如下:

  • 输入是 $y_{j-1}$ :表示第 $(j-1)$ 个单词的独热向量。
  • 输出是 $y_j$ :表示第 $(j-1)$ 个单词的嵌入向量。

每个嵌入向量通过以下公式计算:

$$ y_j = E^{(t)} y_{j-1} $$

其中,$E^{(t)} \in \mathbb{R}^{D \times |\mathcal{V}^{(t)}|}$ 是解码器的嵌入矩阵。


计算例子:

假设以下参数值:

  • 嵌入向量维度 $ D = 3 $
  • 输出词汇表大小 $ |\mathcal{V}^{(t)}| = 4 $

并给定以下独热向量表示的前一个单词:

  • 独热向量 $y_{j-1}$ 对应的是第 $j-1$ 个单词,比如 $y_{j-1} = [0, 1, 0, 0]$ 表示第 $j-1$ 个单词是词汇表中的第二个单词。

假设我们的解码器嵌入矩阵 $E^{(t)}$ 如下:

$$ E^{(t)} = \begin{bmatrix} 0.1 & 0.2 & 0.3 & 0.4 \\ 0.5 & 0.6 & 0.7 & 0.8 \\ 0.9 & 1.0 & 1.1 & 1.2 \\ \end{bmatrix} $$

现在我们来计算输出的嵌入向量 $y_j$。

$$ \begin{split} y_j &= E^{(t)} y_{j-1} \\ &= \begin{bmatrix} 0.1 & 0.2 & 0.3 & 0.4 \\ 0.5 & 0.6 & 0.7 & 0.8 \\ 0.9 & 1.0 & 1.1 & 1.2 \\ \end{bmatrix} \begin{bmatrix} 0 \\ 1 \\ 0 \\ 0 \\ \end{bmatrix} \\ &= \begin{bmatrix} 0.5 \\ 0.6 \\ 0.7 \\ \end{bmatrix} \end{split} $$

因此,根据给定的参数和输入,我们得到了输出的嵌入向量 $y_j = [0.5, 0.6, 0.7]$。


6.4 解码器循环层

回忆一下RNN,图中的 $X$ 就相当于下面即将介绍的$h_j^{(t)}$。

什么是循环神经网络

解码器循环层从嵌入向量生成隐藏向量。在处理第 $j$ 个嵌入向量时,该层的输入和输出如下:

  • 输入是 $y_j$:嵌入向量。
  • 输出是 $h_j^{(t)}$:第 $j$ 个隐藏向量。

例如,当使用单层单向 RNN 时,该过程可以表示为函数 $\Psi^{(t)}$:

$$ \begin{split} h_j^{(t)} &= \Psi^{(t)}(y_j, h_{j-1}^{(t)}) \\ &= \tanh \left( W^{(t)} \left[ \begin{array}{c} h_{j-1}^{(t)} \\ y_j \end{array} \right] + b^{(t)} \right) \end{split} $$

通常 $\Psi$ 符号表示函数,它通常表示为一个激活函数(例如 tanh 或 ReLU)。

在这种情况下,我们使用 $\tanh$ 作为激活函数。我们必须使用编码器最后一个位置的隐藏向量作为解码器第一个位置的隐藏向量,如下所示:

$$ h_0^{(t)} = z = h_I^{(s)} $$

计算例子:

假设以下参数值:

  • 嵌入向量维度 $ D = 3 $
  • 隐藏状态维度 $ H = 4 $

并给定以下嵌入向量和先前的隐藏状态:

  • 嵌入向量 $y_j = [0.5, 0.6, 0.7]$
  • 先前的隐藏状态 $h_{j-1}^{(t)} = [0.1, 0.2, 0.3, 0.4]$

假设我们的权重矩阵 $W^{(t)}$ 和偏置向量 $b^{(t)}$ 如下:

$$ W^{(t)} = \begin{bmatrix} 0.1 & 0.2 & 0.3 & 0.4 & 0.5 & 0.6 & 0.7 \\ 0.8 & 0.9 & 1.0 & 1.1 & 1.2 & 1.3 & 1.4 \\ 1.5 & 1.6 & 1.7 & 1.8 & 1.9 & 2.0 & 2.1 \\ 2.2 & 2.3 & 2.4 & 2.5 & 2.6 & 2.7 & 2.8 \\ \end{bmatrix}, \quad b^{(t)} = \begin{bmatrix} 0.1 \\ 0.2 \\ 0.3 \\ 0.4 \\ \end{bmatrix} $$

现在我们来计算新的隐藏状态 $h_j^{(t)}$。

$$ \begin{split} h_j^{(t)} &= \tanh \left( W^{(t)} \left[ \begin{array}{c} h_{j-1}^{(t)} \\ y_j \end{array} \right] + b^{(t)} \right) \\ &= \tanh \left( W^{(t)} \begin{bmatrix} 0.1 \\ 0.2 \\ 0.3 \\ 0.4 \\ 0.5 \\ 0.6 \\ 0.7 \\ \end{bmatrix} + \begin{bmatrix} 0.1 \\ 0.2 \\ 0.3 \\ 0.4 \\ \end{bmatrix} \right) \\ &= \tanh \left( \begin{bmatrix} 0.1 \times 0.1 + 0.2 \times 0.2 + 0.3 \times 0.3 + 0.4 \times 0.4 + 0.5 \times 0.5 + 0.6 \times 0.6 + 0.7 \times 0.7 \\ 0.8 \times 0.1 + 0.9 \times 0.2 + 1.0 \times 0.3 + 1.1 \times 0.4 + 1.2 \times 0.5 + 1.3 \times 0.6 + 1.4 \times 0.7 \\ 1.5 \times 0.1 + 1.6 \times 0.2 + 1.7 \times 0.3 + 1.8 \times 0.4 + 1.9 \times 0.5 + 2.0 \times 0.6 + 2.1 \times 0.7 \\ 2.2 \times 0.1 + 2.3 \times 0.2 + 2.4 \times 0.3 + 2.5 \times 0.4 + 2.6 \times 0.5 + 2.7 \times 0.6 + 2.8 \times 0.7 \\ \end{bmatrix} + \begin{bmatrix} 0.1 \\ 0.2 \\ 0.3 \\ 0.4 \\ \end{bmatrix} \right) \\ &= \tanh \left( \begin{bmatrix} 0.1 + 0.04 + 0.09 + 0.16 + 0.25 + 0.36 + 0.49 \\ 0.08 + 0.18 + 0.3 + 0.44 + 0.6 + 0.78 + 0.98 \\ 0.15 + 0.32 + 0.51 + 0.72 + 0.95 + 1.2 + 1.47 \\ 0.22 + 0.46 + 0.72 + 1.0 + 1.3 + 1.62 + 1.96 \\ \end{bmatrix} + \begin{bmatrix} 0.1 \\ 0.2 \\ 0.3 \\ 0.4 \\ \end{bmatrix} \right) \\ &= \tanh \left( \begin{bmatrix} 1.49 + 0.1 \\ 3.36 + 0.2 \\ 5.32 + 0.3 \\ 7.28 + 0.4 \\ \end{bmatrix} \right) \\ &= \tanh \left( \begin{bmatrix} 1.59 \\ 3.56 \\ 5.62 \\ 7.68 \\ \end{bmatrix} \right) \\ &= \begin{bmatrix} \tanh(1.59) \\ \tanh(3.56) \\ \tanh(5.62) \\ \tanh(7.68) \\ \end{bmatrix} \\ &= \begin{bmatrix} 0.920 \\ 0.998 \\ 0.999 \\ 0.999 \\ \end{bmatrix} \end{split} $$

因此,根据给定的参数和输入,我们得到了新的隐藏状态 $h_j^{(t)} = [0.920, 0.998, 0.999, 0.999]$。


6.5 解码器输出层

解码器输出层生成输出句子的第 $j$ 个单词。在处理第 $j$ 个嵌入向量时,该层的输入和输出如下:

  • 输入是 $h_j^{(t)}$:第 $j$ 个隐藏向量。
  • 输出是 $p_j$:生成第 $j$ 个单词 $y_j$ 的独热向量的概率。
$$ \begin{split} p_j &= P_{\theta}(y_j | Y_{\lt j}) = \text{softmax}(o_j) \cdot y_j \\ &= \text{softmax}(W^{(o)} h_j^{(t)} + b^{(o)}) \cdot y_j \end{split} $$

其中,softmax 函数用于计算生成独热向量的概率。


计算过程:

假设我们有以下参数:

  • 隐藏状态维度 $H = 4$
  • 输出词汇表的大小 $|\mathcal{V}^{(t)}| = 5$(假设词汇表包含5个单词)

给定以下隐藏状态:

  • $h_j^{(t)} = [0.1, 0.2, 0.3, 0.4]$

假设我们的权重矩阵 $W^{(o)}$ 和偏置向量 $b^{(o)}$ 如下:

$$ W^{(o)} = \begin{bmatrix} 0.1 & 0.2 & 0.3 & 0.4 \\ 0.5 & 0.6 & 0.7 & 0.8 \\ 0.9 & 1.0 & 1.1 & 1.2 \\ 1.3 & 1.4 & 1.5 & 1.6 \\ 0.2 & 0.3 & 0.4 & 0.5 \\ \end{bmatrix}, \quad b^{(o)} = \begin{bmatrix} 0.1 \\ 0.2 \\ 0.3 \\ 0.4 \\ 0.5 \\ \end{bmatrix} $$

我们首先将隐藏状态 $h_j^{(t)}$ 乘以权重矩阵 $W^{(o)}$,然后加上偏置向量 $b^{(o)}$。

最后,我们将结果通过 softmax 函数,得到生成每个单词的概率。

  1. 计算加权输入:
$$ \begin{aligned} o_j &= W^{(o)} h_j^{(t)} + b^{(o)} \\ &= \begin{bmatrix} 0.1 & 0.2 & 0.3 & 0.4 \\ 0.5 & 0.6 & 0.7 & 0.8 \\ 0.9 & 1.0 & 1.1 & 1.2 \\ 1.3 & 1.4 & 1.5 & 1.6 \\ 0.2 & 0.3 & 0.4 & 0.5 \\ \end{bmatrix} \begin{bmatrix} 0.1 \\ 0.2 \\ 0.3 \\ 0.4 \\ \end{bmatrix} + \begin{bmatrix} 0.1 \\ 0.2 \\ 0.3 \\ 0.4 \\ 0.5 \\ \end{bmatrix} \\ &= \begin{bmatrix} 0.1 \times 0.1 + 0.2 \times 0.2 + 0.3 \times 0.3 + 0.4 \times 0.4 + 0.1 \\ 0.5 \times 0.1 + 0.6 \times 0.2 + 0.7 \times 0.3 + 0.8 \times 0.4 + 0.2 \\ 0.9 \times 0.1 + 1.0 \times 0.2 + 1.1 \times 0.3 + 1.2 \times 0.4 + 0.3 \\ 1.3 \times 0.1 + 1.4 \times 0.2 + 1.5 \times 0.3 + 1.6 \times 0.4 + 0.4 \\ 0.2 \times 0.1 + 0.3 \times 0.2 + 0.4 \times 0.3 + 0.5 \times 0.4 + 0.5 \\ \end{bmatrix} \\ &= \begin{bmatrix} 0.3 \\ 1.2 \\ 2.1 \\ 3.0 \\ 0.6 \\ \end{bmatrix} \end{aligned} $$
  1. 应用 softmax 函数:
$$ \text{softmax}(o_j) = \text{softmax} \left( \begin{bmatrix} 0.3 \\ 1.2 \\ 2.1 \\ 3.0 \\ 0.6 \end{bmatrix} \right) $$

Softmax 函数的定义是:

$$ \text{softmax}(z)_i = \frac{e^{z_i}}{\sum_{j=1}^N e^{z_j}} $$

对于我们的例子,我们计算:

$$ \begin{aligned} \text{softmax}(o_j)_1 &= \frac{e^{0.3}}{e^{0.3} + e^{1.2} + e^{2.1} + e^{3.0} + e^{0.6}} \\ &\approx \frac{1.3499}{1.3499 + 3.3201 + 8.1662 + 20.0855 + 1.8221} \\ &\approx \frac{1.3499}{34.7438} \\ &\approx 0.0388 \end{aligned} $$ $$ \begin{aligned} \text{softmax}(o_j)_2 &= \frac{e^{1.2}}{e^{0.3} + e^{1.2} + e^{2.1} + e^{3.0} + e^{0.6}} \\ &\approx \frac{3.3201}{1.3499 + 3.3201 + 8.1662 + 20.0855 + 1.8221} \\ &\approx \frac{3.3201}{34.7438} \\ &\approx 0.0955 \end{aligned} $$

以此类推,我们计算出所有类别的概率。

  1. 最后,我们计算生成每个单词的概率 $p_j$,根据给定的当前单词向量 $y_j$。假设我们的目标单词是第 2 个单词,即 $y_j = [0, 1, 0, 0, 0]$,那么:$$ \begin{aligned} p_j &= \text{softmax}(o_j) \cdot y_j \\&= [0.0388, 0.0955, 0, 0, 0] \cdot [0, 1, 0, 0, 0] \\&= 0.0955 \end{aligned} $$

因此,根据给定的参数和输入,生成第 2 个单词的概率 $p_j$ 为 0.0955。


6.6 模型架构替换

Seq2Seq 模型除了可以使用 RNN 实现,还可以使用 LSTM 实现。

LSTM 作为一种特殊类型的 RNN,能够更有效地捕捉序列数据中的长程依赖关系,因此在处理更复杂和更长的输入序列时表现更好。通过将 LSTM 用于编码器和解码器,Seq2Seq 模型可以增强其对长序列和复杂依赖关系的处理能力,从而提升整体性能和鲁棒性。

查看 lstm-rnn 的源文件

6.6.1 使用 LSTM 作为 Encoder

在 Encoder 部分,LSTM 接收输入序列,并将其转换为一个固定大小的上下文向量(或隐藏状态):

  1. 编码器嵌入层:将输入句子的每个单词转换为嵌入向量。
  2. 编码器 LSTM 层:处理嵌入向量序列,生成隐藏状态和细胞状态。

示例公式:

$$ h_i^{(s)}, c_i^{(s)} = \text{LSTM}(x_i, h_{i-1}^{(s)}, c_{i-1}^{(s)}) $$
6.6.2 使用 LSTM 作为 Decoder

在 Decoder 部分,LSTM 接收前一步的输出和编码器的上下文向量(或隐藏状态),并生成当前步骤的输出:

  1. 解码器嵌入层:将解码器前一步生成的单词转换为嵌入向量。
  2. 解码器 LSTM 层:处理嵌入向量和前一步的隐藏状态,生成新的隐藏状态和细胞状态。
  3. 解码器输出层:将隐藏状态转换为输出概率分布。

示例公式:

$$ h_j^{(t)}, c_j^{(t)} = \text{LSTM}(y_j, h_{j-1}^{(t)}, c_{j-1}^{(t)}) $$

通过使用 LSTM 作为 Encoder 和 Decoder,Seq2Seq 模型可以更好地处理长序列和复杂的依赖关系,提升模型的性能和鲁棒性。

第二章 代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
import torch
import torch.nn as nn
import torch.optim as optim

# 定义编码器模型
class Encoder(nn.Module):
def __init__(self, input_size, hidden_size):
super(Encoder, self).__init__()
self.hidden_size = hidden_size
# 初始化嵌入层,将输入序列中的每个单词映射到一个向量表示
self.embedding = nn.Embedding(input_size, hidden_size)
# 初始化LSTM循环层,处理嵌入向量序列并输出隐藏状态
self.rnn = nn.LSTM(hidden_size, hidden_size)

def forward(self, input, hidden):
# 将输入序列中的每个单词转换为嵌入向量
embedded = self.embedding(input).view(1, 1, -1)
# 将嵌入向量序列输入到LSTM循环层中,并返回输出和隐藏状态
output, hidden = self.rnn(embedded, hidden)
return output, hidden

def init_hidden(self):
# 初始化编码器的初始隐藏状态
return (torch.zeros(1, 1, self.hidden_size), torch.zeros(1, 1, self.hidden_size))

# 定义解码器模型
class Decoder(nn.Module):
def __init__(self, hidden_size, output_size):
super(Decoder, self).__init__()
self.hidden_size = hidden_size
# 初始化嵌入层,将输出序列中的每个单词映射到一个向量表示
self.embedding = nn.Embedding(output_size, hidden_size)
# 初始化LSTM循环层,处理嵌入向量序列并输出隐藏状态
self.rnn = nn.LSTM(hidden_size, hidden_size)
# 初始化输出层,将隐藏状态映射到输出词汇表中的单词概率分布
self.out = nn.Linear(hidden_size, output_size)
# 初始化softmax函数,将输出转换为概率分布
self.softmax = nn.LogSoftmax(dim=1)

def forward(self, input, hidden):
# 将输入序列中的每个单词转换为嵌入向量
output = self.embedding(input).view(1, 1, -1)
# 应用ReLU激活函数
output = nn.functional.relu(output)
# 将嵌入向量序列输入到LSTM循环层中,并返回输出和隐藏状态
output, hidden = self.rnn(output, hidden)
# 将LSTM的输出通过输出层和softmax函数,得到单词的概率分布
output = self.softmax(self.out(output[0]))
return output, hidden

# 训练函数
def train(input_tensor, target_tensor, encoder, decoder, encoder_optimizer, decoder_optimizer, criterion, max_length=10):
# 初始化编码器的隐藏状态
encoder_hidden = encoder.init_hidden()

# 清空优化器的梯度
encoder_optimizer.zero_grad()
decoder_optimizer.zero_grad()

input_length = input_tensor.size(0)
target_length = target_tensor.size(0)

loss = 0

# 编码器的前向传播
for ei in range(input_length):
encoder_output, encoder_hidden = encoder(
input_tensor[ei], encoder_hidden)

# 初始化解码器的输入,使用特殊的起始标记
decoder_input = torch.tensor([[SOS_token]])

# 解码器的初始隐藏状态为编码器的最终隐藏状态
decoder_hidden = encoder_hidden

# 解码器的前向传播
for di in range(target_length):
decoder_output, decoder_hidden = decoder(
decoder_input, decoder_hidden)
topv, topi = decoder_output.topk(1)
decoder_input = topi.squeeze().detach() # 前一个输出作为下一个输入

loss += criterion(decoder_output, target_tensor[di])
if decoder_input.item() == EOS_token:
break

# 反向传播和优化
loss.backward()

encoder_optimizer.step()
decoder_optimizer.step()

return loss.item() / target_length

# 使用一个简单的数据集进行训练
input_tensor = torch.tensor([[0], [1], [2], [3], [4]])
target_tensor = torch.tensor([[0], [1], [2], [3], [4]])

# 定义参数
input_size = 5
output_size = 5
hidden_size = 64
SOS_token = 0
EOS_token = 4

# 初始化模型、优化器和损失函数
encoder = Encoder(input_size, hidden_size)
decoder = Decoder(hidden_size, output_size)
encoder_optimizer = optim.SGD(encoder.parameters(), lr=0.01)
decoder_optimizer = optim.SGD(decoder.parameters(), lr=0.01)
criterion = nn.NLLLoss()

# 训练模型
n_iters = 100
for iter in range(1, n_iters + 1):
loss = train(input_tensor, target_tensor, encoder, decoder, encoder_optimizer, decoder_optimizer, criterion)
print('Iteration: {}, Loss: {}'.format(iter, loss))