Encoder-Decoder

ZhuYuanxiang 2021-04-20 18:07:25
Categories: Tags:

9. Modern Recurrent Neural Networks

9.7. Sequence to Sequence Learning

在机器翻译中,输入序列和输出序列都是长度可变的,使用两个 RNN 来设计「编码器-解码器」结构,以可变长度序列作为输入,将其转换为固定形状的隐藏状态。输入序列(源)的信息被 编码 到 RNN 编码器的隐藏状态中,输出序列(目标)的信息在 RNN 解码器基于输入序列的编码信息和已经生成的标记来预测下一个标记。序列中 <bos> 表示序列的开始;<eos> 表示序列的结束;<unk> 表示未知的数据。

9.7.1. Encoder

编码器将可变长度的输入序列转换成固定形状的 上下文变量 $\mathbf{c}$;并且将输入序列的信息编码在这个上下文变量中。

假设一个序列样本(批量大小:1),输入序列是$x_1,\ldots,x_T$,其中 $x_t$ 是输入序列中的第 $t$ 个标记。在时间步 $t$,RNN将用于 $x_t$ 的输入特征向量 $\mathbf{x}t$ 和来自上一时间步的隐藏状态 $\mathbf{h}{t-1}$ 转换为当前隐藏状态 $\mathbf{h}_t$。用一个函数 $f$ 来表示 RNN 层所做的变换:
$$
\mathbf{h}_t=f(\mathbf{x}t,\mathbf{h}{t-1})
$$
编码器通过特定的函数 $q$ 将所有时间步的隐藏状态转换为上下文变量:
$$
\mathbf{c}=q(\mathbf{h}_1,\ldots,\mathbf{h}_T)
$$
使用单向 RNN 来设计编码器,其中隐藏状态依赖于输入的子序列,子序列由序列的开始位置到隐藏状态所在的时间步的位置决定(包括当前时间步的输入)。

Todo:使用双向 RNN 来设计编码器,其中隐藏状态依赖于输入的两个子序列,分别是隐藏状态所在的时间步的位置之前的和之后的子序列(包括当前时间步处的输入),这个子序列完成了对整个序列的信息编码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Seq2SeqEncoder(d2l.Encoder):
"""用于序列到序列学习的 RNN 编码器"""
def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,dropout=0, **kwargs):
super(Seq2SeqEncoder, self).__init__(**kwargs)
self.embedding = nn.Embedding(vocab_size, embed_size) # 嵌入层
self.rnn = rnn.GRU(num_hiddens, num_layers, dropout=dropout)

def forward(self, X, *args):
# 输出'X'的形状:(`batch_size`, `num_steps`, `embed_size`)
X = self.embedding(X)
# 在RNN 模型中,第一个轴对应于时间步长
X = X.swapaxes(0, 1)
state = self.rnn.begin_state(batch_size=X.shape[1], ctx=X.ctx)
output, state = self.rnn(X, state)
# `output`的形状: (`num_steps`, `batch_size`, `num_hiddens`)
# `state[0]`的形状: (`num_layers`, `batch_size`, `num_hiddens`)
return output, state

10. Attention Mechanisms

10.2. Attention Pooling: Nadaraya-Watson Kernel Regression

查询(自主提示)和键(非自主提示)之间的交互产生了「注意力池化」,从而有选择性地聚合了值(感官输入)以产生输出。

10.2.3. Nonparametric Attention Pooling

使用 Nadaraya-Watson 核回归,核为 $K(u)=\frac1{\sqrt{2\pi}}\exp(-\frac{u^2}2)$,根据输入的位置对输出进行权衡:
$$
\begin{aligned}
f(x) &= \sum_{i=1}^n \frac{K(x - x_i)}{\sum_{j=1}^n K(x - x_j)} y_i\
&=\sum_{i=1}^n\alpha(x,x_i)y_i\
&=\sum_{i=1}^n\frac{\exp(-\frac12(x-x_i)^2)}{\sum_{j=1}^n\exp(-\frac12(x-x_j)^2)}y_i\
&=\sum_{i=1}^n\mathrm{Softmax}(-\frac12(x-x_i)^2)y_i
\end{aligned}
$$

1
2
3
4
5
6
7
8
9
10
11
12
# X_repeat 的形状: (n_test, n_train), 用于批量计算 $f(x)$,而不是一个个计算
# 每一行都包含着相同的测试输入,即同样的查询
X_repeat = x_test.repeat(n_train).reshape((-1, n_train))
# x_test 包含的是查询;x_train 包含的是键;y_train 包含的是值。
# attention_weights 的形状:(n_test, n_train),
# 每一行包含的是在给定的每个键 (x_train) 对应的值 (y_train) 之间应该分配的注意力权重
# 当查询与键越接近时,注意力池化的注意力权重就越高
# 用于计算查询 (x_test) 对应的值 (y_hat) 的输出
# 注:由于输入的查询受所有的键和当前位置的值的影响,因此同样的查询输出不同的值
attention_weights = npx.softmax(-(X_repeat - x_train) ** 2 / 2)
# y_hat 的每个元素都是值的加权平均值,其中的权重是注意力权重
y_hat = np.dot(attention_weights, y_train)

10.2.4. Parametric Attention Pooling

$$
\begin{aligned}
f(x) &= \sum_{i=1}^n \alpha(x, x_i)y_i \
&= \sum_{i=1}^n \frac{\exp\left(-\frac{1}{2}((x - x_i)w)^2\right)}{\sum_{j=1}^n \exp\left(-\frac{1}{2}((x - x_i)w)^2\right)} y_i \
&= \sum_{i=1}^n \mathrm{softmax}\left(-\frac{1}{2}((x - x_i)w)^2\right) y_i.
\end{aligned}
$$

10.2.4.1. Batch Matrix Multiplication

批处理乘法用于提升小批量注意力计算的效率。

假设第一个小批量包含 $n$ 个矩阵 $\mathbf{X}_1,\ldots, \mathbf{X}_n$,形状为 $a\times b$,第二个小批量包含 $n$ 个矩阵 $\mathbf{Y}_1, \ldots, \mathbf{Y}_n$,形状为 $b\times c$。它们的批量矩阵乘法得出 $n$ 个矩阵 $\mathbf{X}_1\mathbf{Y}_1, \ldots, \mathbf{X}_n\mathbf{Y}_n$,形状为 $a\times c$。因此,假定两个张量的形状 $(n,a,b)$ 和 $(n,b,c)$ ,它们的批量矩阵乘法输出的形状为 $(n,a,c)$。

10.2.4.2. Defining the Model

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class NWKernelRegression(nn.Block):
def __init__(self, **kwargs):
super(NWKernelRegression, self).__init__(**kwargs)
self.attention_weights = None
self.w = self.params.get('w', shape=(1,))

def forward(self, queries, keys, values):
# 'queries' 的形状: (查询的个数,“键-值”对的个数)
queries = queries.repeat(keys.shape[1]).reshape((-1, keys.shape[1]))
# 'attention_weights' 的形状: (查询的个数,“键-值”对的个数)
self.attention_weights = npx.softmax(-((queries - keys) * self.w.data()) ** 2 / 2)
# 'values' 的形状: (查询的个数,“键-值”对的个数)
return npx.batch_dot(np.expand_dims(self.attention_weights, 1),
np.expand_dims(values, -1)).reshape(-1)

10.2.4.3. Training

1
2
3
4
5
6
7
8
9
10
net = NWKernelRegression()
net.initialize()
loss = gluon.loss.L2Loss()
trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': 0.5})
animator = d2l.Animator(xlabel='epoch', ylabel='loss', xlim=[1, 5])
for epoch in range(50):
with autograd.record():
l = loss(net(x_train, keys, values), y_train)
l.backward()
trainer.step(1)

10.3. Attention Scoring Functions

使用高斯核对查询和键之间的关系建模,可以将高斯核的指数部分视为 注意力评分函数(简称 评分函数)。

用数学语言描述,假设查询 $\mathbf{q} \in \mathbb{R}^q$ 和 “键-值”对 $(\mathbf{k}_1, \mathbf{v}_1), \ldots, (\mathbf{k}_m, \mathbf{v}_m), \mathbf{k}_i \in \mathbb{R}^k, \mathbf{v}_i \in \mathbb{R}^v$。注意力池化函数 $f$ 被表示成值的加权和:

$$
f(\mathbf{q}, (\mathbf{k}_1, \mathbf{v}_1), \ldots, (\mathbf{k}m, \mathbf{v}m)) = \sum{i=1}^m \alpha(\mathbf{q}, \mathbf{k}_i) \mathbf{v}_i \in \mathbb{R}^v,
$$

其中查询 $\mathbf{q}$ 和键 $\mathbf{k}_i$ 的注意力权重(标量)是通过注意力评分函数 $a$ 的 softmax 运算得到的,该函数将两个向量映射成标量:

$$
\alpha(\mathbf{q}, \mathbf{k}_i) = \mathrm{softmax}(a(\mathbf{q}, \mathbf{k}i)) = \frac{\exp(a(\mathbf{q}, \mathbf{k}i))}{\sum{j=1}^m \exp(a(\mathbf{q}, \mathbf{k}_j))} \in \mathbb{R}.
$$

选择不同的注意力评分函数 $a$ 导致不同的注意力池化操作。

本节将会介绍两个注意力评分函数:

10.3.1. Masked Softmax Operation

Softmax 运算用于输出一个概率分布作为注意力权重。为了在 Softmax 计算时过滤掉用于填充序列的没有意义的特殊标记,可以指定一个有效序列长度(即标记的个数),然后使用 masked_softmax 函数将无效的标记遮盖并输出为零。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def masked_softmax(X, valid_lens):
"""通过在最后一个轴上遮盖元素来执行 Softmax 操作"""
# `X`: 3D tensor, `valid_lens`: 1D or 2D tensor
if valid_lens is None:
return npx.softmax(X)
else:
shape = X.shape
if valid_lens.ndim == 1:
valid_lens = valid_lens.repeat(shape[1])
else:
valid_lens = valid_lens.reshape(-1)
# 在最后的轴上,被遮盖的元素使用一个非常大的负值替换,从而其 softmax (指数)输出为 0
X = npx.sequence_mask(X.reshape(-1, shape[-1]), sequence_length=valid_lens,
use_sequence_length=True, value=-1e6, axis=1)
return npx.softmax(X).reshape(shape)

masked_softmax(np.random.uniform(size=(2, 2, 4)), d2l.tensor([2, 3]))

10.3.2. Additive Attention

当查询和键是不同长度的矢量时,可以使用可加性注意力作为评分函数。
$$
a(\mathbf q, \mathbf k) = \text{tanh}(\mathbf W_q\mathbf q + \mathbf W_k \mathbf k) \mathbf w_v^\top \in \mathbb{R}
$$
其中,查询 $\mathbf{q} \in \mathbb{R}^q$ ,键 $\mathbf{k} \in \mathbb{R}^k$,可学习的参数 $\mathbf W_q\in\mathbb R^{h\times q}$、$\mathbf W_k\in\mathbb R^{h\times k}$ 和 $\mathbf w_v\in\mathbb R^{h}$,隐藏层的单元个数是超参数 $h$。

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
class AdditiveAttention(nn.Block):
"""可加性注意力"""
def __init__(self, num_hiddens, dropout, **kwargs):
super(AdditiveAttention, self).__init__(**kwargs)
# 使用' flatten=False '只转换最后一个轴,以便其他轴的形状保持不变
self.W_k = nn.Dense(num_hiddens, use_bias=False, flatten=False)
self.W_q = nn.Dense(num_hiddens, use_bias=False, flatten=False)
self.w_v = nn.Dense(1, use_bias=False, flatten=False)
self.dropout = nn.Dropout(dropout)

def forward(self, queries, keys, values, valid_lens):
queries, keys = self.W_q(queries), self.W_k(keys)
# 在维度扩展后,
# `queries` 的形状:(`batch_size`, 查询的个数, 1, `num_hidden`)
# `key` 的形状:(`batch_size`, 1, “键-值”对的个数, `num_hiddens`)
# 使用广播的方式进行求和
features = np.expand_dims(queries, axis=2) + np.expand_dims(keys, axis=1)
features = np.tanh(features)
# `self.w_v` 仅有一个输出,因此从形状中移除最后那个维度。
# `scores` 的形状:(`batch_size`, 查询的个数, “键-值”对的个数)
scores = np.squeeze(self.w_v(features), axis=-1)
self.attention_weights = masked_softmax(scores, valid_lens)
# `values` 的形状:(`batch_size`, “键-值”对的个数, 值的维度)
return npx.batch_dot(self.dropout(self.attention_weights), values)

attention = AdditiveAttention(num_hiddens=8, dropout=0.1)
attention.initialize()
attention(queries, keys, values, valid_lens)

10.3.3. Scaled Dot-Product Attention

“点-积”操作要求查询和键具有相同的矢量长度 $d$,但是计算效率更高。假设查询和键的所有元素都是独立的随机变量,并且都满足均值为 $0$ 和方差为 $1$,那么两个矢量的“点-积”满足均值为 $0$和方差为 $d$。为了确保方差不受矢量长度的影响,可以将之除以 $\sqrt{d}$,则新的 缩放的“点-积”注意力 评分函数的方差为 $1$。
$$
a(\mathbf q, \mathbf k) = \mathbf{q}^\top \mathbf{k} /\sqrt{d}
$$
在实践中,常常基于小批量的角度来提高效率,例如:基于 $n$ 个查询和 $m$ 个“键-值”对计算注意力,其中查询和键的长度为 $d$,值的长度为 $v$。查询 $\mathbf Q\in\mathbb R^{n\times d}$、键 $\mathbf K\in\mathbb R^{m\times d}$ 和值 $\mathbf V\in\mathbb R^{m\times v}$ 的缩放的“点-积”注意力是

$$
\mathrm{softmax}\left(\frac{\mathbf Q \mathbf K^\top }{\sqrt{d}}\right) \mathbf V \in \mathbb{R}^{n\times v}.
$$
在下面的缩放的“点-积”注意力的实现中,使用了 dropout 进行模型正则化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class DotProductAttention(nn.Block):
"""Scaled dot product attention."""
def __init__(self, dropout, **kwargs):
super(DotProductAttention, self).__init__(**kwargs)
self.dropout = nn.Dropout(dropout)

# `queries` 的形状:(`batch_size`, 查询的个数, `d`)
# `keys` 的形状:(`batch_size`, “键-值”对的个数, `d`)
# `values` 的形状:(`batch_size`, “键-值”对的个数, 值的维度)
# `valid_lens` 的形状: (`batch_size`,) 或者 (`batch_size`, 查询的个数)
def forward(self, queries, keys, values, valid_lens=None):
d = queries.shape[-1]
# 设置 `transpose_b=True` 为了交换 `keys` 的最后两个维度
scores = npx.batch_dot(queries, keys, transpose_b=True) / math.sqrt(d)
self.attention_weights = masked_softmax(scores, valid_lens)
return npx.batch_dot(self.dropout(self.attention_weights), values)

attention = DotProductAttention(dropout=0.5)
attention.initialize()
attention(queries, keys, values, valid_lens)

10.4. Bahdanau Attention

没有对齐方向限制的可区分的注意力模型。在预测标记时,如果不是所有的输入标记都相关,模型将仅对齐(或者注意)输入序列中与当前预测相关的部分。这是通过将上下文变量视为注意力池化的输出来实现的。

10.4.1. Model