doc:修正 第二章共识排版

This commit is contained in:
KMnO4-zx
2025-05-13 21:19:31 +08:00
parent 78309523d5
commit 786c77e2ea

View File

@@ -73,35 +73,49 @@
那么,我们最终查询到的值应该是:
$$value = 0.6 * 10 + 0.4 * 5 + 0 * 2 = 8$$
$$
value = 0.6 * 10 + 0.4 * 5 + 0 * 2 = 8
$$
给不同 Key 所赋予的不同权重,就是我们所说的注意力分数,也就是为了查询到 Query我们应该赋予给每一个 Key 多少注意力。但是,如何针对每一个 Query计算出对应的注意力分数呢从直观上讲我们可以认为 Key 与 Query 相关性越高,则其所应该赋予的注意力权重就越大。但是,我们如何能够找到一个合理的、能够计算出正确的注意力分数的方法呢?
在第一章中,我们有提到词向量的概念。通过合理的训练拟合,词向量能够表征语义信息,从而让语义相近的词在向量空间中距离更近,语义较远的词在向量空间中距离更远。我们往往用欧式距离来衡量词向量的相似性,但我们同样也可以用点积来进行度量:
$$v·w = \sum_{i}v_iw_i$$
$$
v·w = \sum_{i}v_iw_i
$$
根据词向量的定义语义相似的两个词对应的词向量的点积应该大于0而语义不相似的词向量点积应该小于0。
那么,我们就可以用点积来计算词之间的相似度。假设我们的 Query 为“fruit”对应的词向量为 $q$;我们的 Key 对应的词向量为 $k = [v_{apple} v_{banana} v_{chair}]$,则我们可以计算 Query 和每一个键的相似程度:
$$x = qK^T$$
$$
x = qK^T
$$
此处的 K 即为将所有 Key 对应的词向量堆叠形成的矩阵。基于矩阵乘法的定义x 即为 q 与每一个 k 值的点积。现在我们得到的 x 即反映了 Query 和每一个 Key 的相似程度,我们再通过一个 Softmax 层将其转化为和为 1 的权重:
$$softmax(x)_i = \frac{e^{xi}}{\sum_{j}e^{x_j}}$$
$$
softmax(x)_i = \frac{e^{xi}}{\sum_{j}e^{x_j}}
$$
这样,得到的向量就能够反映 Query 和每一个 Key 的相似程度,同时又相加权重为 1也就是我们的注意力分数了。最后我们再将得到的注意力分数和值向量做对应乘积即可。根据上述过程我们就可以得到注意力机制计算的基本公式
$$attention(Q,K,V) = softmax(qK^T)v$$
$$
attention(Q,K,V) = softmax(qK^T)v
$$
不过,此时的值还是一个标量,同时,我们此次只查询了一个 Query。我们可以将值转化为维度为 $d_v$ 的向量,同时一次性查询多个 Query同样将多个 Query 对应的词向量堆叠在一起形成矩阵 Q得到公式
$$attention(Q,K,V) = softmax(QK^T)V$$
$$
attention(Q,K,V) = softmax(QK^T)V
$$
目前,我们离标准的注意力机制公式还差最后一步。在上一个公式中,如果 Q 和 K 对应的维度 $d_k$ 比较大softmax 放缩时就非常容易受影响,使不同值之间的差异较大,从而影响梯度的稳定性。因此,我们要将 Q 和 K 乘积的结果做一个放缩:
$$attention(Q,K,V) = softmax(\frac{QK^T}{\sqrt{d_k}})V$$
$$
attention(Q,K,V) = softmax(\frac{QK^T}{\sqrt{d_k}})V
$$
这也就是注意力机制的核心计算公式了。
@@ -141,7 +155,7 @@ def attention(query, key, value, dropout=None):
但是,在我们的实际应用中,我们往往只需要计算 Query 和 Key 之间的注意力结果,很少存在额外的真值 Value。也就是说我们其实只需要拟合两个文本序列。在经典的 注意力机制中Q 往往来自于一个序列K 与 V 来自于另一个序列,都通过参数矩阵计算得到,从而可以拟合这两个序列之间的关系。例如在 Transformer 的 Decoder 结构中Q 来自于 Encoder 的输出K 与 V 来自于 Decoder 的输入,从而拟合了编码信息与历史信息之间的关系,便于综合这两种信息实现未来的预测。
​但在 Transformer 的 Encoder 结构中,使用的是 注意力机制的变种 —— 自注意力self-attention自注意力机制。所谓自注意力即是计算本身序列中每个元素都其他元素的注意力分布即在计算过程中Q、K、V 都由同一个输入通过不同的参数矩阵计算得到。在 Encoder 中Q、K、V 分别是输入对参数矩阵 $W_q$、$W_k$、$W_v$ 做积得到,从而拟合输入语句中每一个 token 对其他所有 token 的关系。
​但在 Transformer 的 Encoder 结构中,使用的是 注意力机制的变种 —— 自注意力self-attention自注意力机制。所谓自注意力即是计算本身序列中每个元素都其他元素的注意力分布即在计算过程中Q、K、V 都由同一个输入通过不同的参数矩阵计算得到。在 Encoder 中Q、K、V 分别是输入对参数矩阵 $W_q、W_k、W_v$ 做积得到,从而拟合输入语句中每一个 token 对其他所有 token 的关系。
通过自注意力机制,我们可以找到一段文本中每一个 token 与其他所有 token 的相关关系大小从而建模文本之间的依赖关系。在代码中的实现self-attention 机制其实是通过给 Q、K、V 的输入传入同一个参数实现的:
@@ -375,17 +389,23 @@ class MLP(nn.Module):
因此,在深度神经网络中,往往需要归一化操作,将每一层的输入都归一化成标准正态分布。批归一化是指在一个 mini-batch 上进行归一化,相当于对一个 batch 对样本拆分出来一部分,首先计算样本的均值:
$$\mu_j = \frac{1}{m}\sum^{m}_{i=1}Z_j^{i}$$
$$
\mu_j = \frac{1}{m}\sum^{m}_{i=1}Z_j^{i}
$$
其中,$Z_j^{i}$ 是样本 i 在第 j 个维度上的值m 就是 mini-batch 的大小。
再计算样本的方差:
$$\sigma^2 = \frac{1}{m}\sum^{m}_{i=1}(Z_j^i - \mu_j)^2$$
$$
\sigma^2 = \frac{1}{m}\sum^{m}_{i=1}(Z_j^i - \mu_j)^2
$$
最后,对每个样本的值减去均值再除以标准差来将这一个 mini-batch 的样本的分布转化为标准正态分布:
$$\widetilde{Z_j} = \frac{Z_j - \mu_j}{\sqrt{\sigma^2 + \epsilon}}$$
$$
\widetilde{Z_j} = \frac{Z_j - \mu_j}{\sqrt{\sigma^2 + \epsilon}}
$$
此处加上 $\epsilon$ 这一极小量是为了避免分母为0。
@@ -424,9 +444,11 @@ class LayerNorm(nn.Module):
由于 Transformer 模型结构较复杂、层数较深为了避免模型退化Transformer 采用了残差连接的思想来连接每一个子层。残差连接,即下一层的输入不仅是上一层的输出,还包括上一层的输入。残差连接允许最底层信息直接传到最高层,让高层专注于残差的学习。
​例如,在 Encoder 中,在第一个子层,输入进入多头自注意力层的同时会直接传递到该层的输出,然后该层的输出会与原输入相加,再进行标准化。在第二个子层也是一样。即:
$$
x = x + MultiHeadSelfAttention(LayerNorm(x))
$$
$$
output = x + FNN(LayerNorm(x))
$$
@@ -552,20 +574,24 @@ class Decoder(nn.Module):
Embedding 层其实是一个存储固定大小的词典的嵌入向量查找表。也就是说,在输入神经网络之前,我们往往会先让自然语言输入通过分词器 tokenizer分词器的作用是把自然语言输入切分成 token 并转化成一个固定的 index。例如如果我们将词表大小设为 4输入“我喜欢你”那么分词器可以将输入转化成
input: 我
output: 0
```
input:
output: 0
input: 喜欢
output: 1
input: 喜欢
output: 1
input
output: 2
input
output: 2
```
当然在实际情况下tokenizer 的工作会比这更复杂。例如,分词有多种不同的方式,可以切分成词、切分成子词、切分成字符等,而词表大小则往往高达数万数十万。此处我们不赘述 tokenizer 的详细情况,在后文会详细介绍大模型的 tokenizer 是如何运行和训练的。
因此Embedding 层的输入往往是一个形状为 batch_sizeseq_len1的矩阵第一个维度是一次批处理的数量第二个维度是自然语言序列的长度第三个维度则是 token 经过 tokenizer 转化成的 index 值。例如对上述输入Embedding 层的输入会是:
[[0,1,2]]
```
[[0,1,2]]
```
其 batch_size 为1seq_len 为3转化出来的 index 如上。
@@ -593,13 +619,17 @@ $$
上式中pos 为 token 在句子中的位置2i 和 2i+1 则是指示了 token 是奇数位置还是偶数位置,从上式中我们可以看出对于奇数位置的 token 和偶数位置的 tokenTransformer 采用了不同的函数进行编码。
我们以一个简单的例子来说明位置编码的计算过程:假如我们输入的是一个长度为 4 的句子"I like to code",我们可以得到下面的词向量矩阵$\rm x$,其中每一行代表的就是一个词向量,$\rm x_0=[0.1,0.2,0.3,0.4]$对应的就是“I”的词向量它的pos就是为0以此类推第二行代表的是“like”的词向量它的pos就是1
$$
\rm x = \begin{bmatrix} 0.1 & 0.2 & 0.3 & 0.4 \\ 0.2 & 0.3 & 0.4 & 0.5 \\ 0.3 & 0.4 & 0.5 & 0.6 \\ 0.4 & 0.5 & 0.6 & 0.7 \end{bmatrix}
$$
​则经过位置编码后的词向量为:
$$
\rm x_{PE} = \begin{bmatrix} 0.1 & 0.2 & 0.3 & 0.4 \\ 0.2 & 0.3 & 0.4 & 0.5 \\ 0.3 & 0.4 & 0.5 & 0.6 \\ 0.4 & 0.5 & 0.6 & 0.7 \end{bmatrix} + \begin{bmatrix} \sin(\frac{0}{10000^0}) & \cos(\frac{0}{10000^0}) & \sin(\frac{0}{10000^{2/4}}) & \cos(\frac{0}{10000^{2/4}}) \\ \sin(\frac{1}{10000^0}) & \cos(\frac{1}{10000^0}) & \sin(\frac{1}{10000^{2/4}}) & \cos(\frac{1}{10000^{2/4}}) \\ \sin(\frac{2}{10000^0}) & \cos(\frac{2}{10000^0}) & \sin(\frac{2}{10000^{2/4}}) & \cos(\frac{2}{10000^{2/4}}) \\ \sin(\frac{3}{10000^0}) & \cos(\frac{3}{10000^0}) & \sin(\frac{3}{10000^{2/4}}) & \cos(\frac{3}{10000^{2/4}}) \end{bmatrix} = \begin{bmatrix} 0.1 & 1.2 & 0.3 & 1.4 \\ 1.041 & 0.84 & 0.41 & 1.49 \\ 1.209 & -0.016 & 0.52 & 1.59 \\ 0.541 & -0.489 & 0.895 & 1.655 \end{bmatrix}
$$
我们可以使用如下的代码来获取上述例子的位置编码:
```python
import numpy as np
@@ -616,12 +646,14 @@ def PositionEncoding(seq_len, d_model, n=10000):
P = PositionEncoding(seq_len=4, d_model=4, n=100)
print(P)
```
```python
[[ 0. 1. 0. 1. ]
[ 0.84147098 0.54030231 0.09983342 0.99500417]
[ 0.90929743 -0.41614684 0.19866933 0.98006658]
[ 0.14112001 -0.9899925 0.29552021 0.95533649]]
```
这样的位置编码主要有两个好处:
1. 使 PE 能够适应比训练集里面所有句子更长的句子,假设训练集里面最长的句子是有 20 个单词,突然来了一个长度为 21 的句子,则使用公式计算的方法可以计算出第 21 位的 Embedding。
@@ -629,50 +661,70 @@ print(P)
我们也可以通过严谨的数学推导证明该编码方式的优越性。原始的 Transformer Embedding 可以表示为:
$$\begin{equation}f(\cdots,\boldsymbol{x}_m,\cdots,\boldsymbol{x}_n,\cdots)=f(\cdots,\boldsymbol{x}_n,\cdots,\boldsymbol{x}_m,\cdots)\end{equation}
$$
\begin{equation}f(\cdots,\boldsymbol{x}_m,\cdots,\boldsymbol{x}_n,\cdots)=f(\cdots,\boldsymbol{x}_n,\cdots,\boldsymbol{x}_m,\cdots)\end{equation}
$$
很明显,这样的函数是不具有不对称性的,也就是无法表征相对位置信息。我们想要得到这样一种编码方式:
$$\begin{equation}\tilde{f}(\cdots,\boldsymbol{x}_m,\cdots,\boldsymbol{x}_n,\cdots)=f(\cdots,\boldsymbol{x}_m + \boldsymbol{p}_m,\cdots,\boldsymbol{x}_n + \boldsymbol{p}_n,\cdots)\end{equation}
$$
\begin{equation}\tilde{f}(\cdots,\boldsymbol{x}_m,\cdots,\boldsymbol{x}_n,\cdots)=f(\cdots,\boldsymbol{x}_m + \boldsymbol{p}_m,\cdots,\boldsymbol{x}_n + \boldsymbol{p}_n,\cdots)\end{equation}
$$
这里加上的 $p_m$$p_n$ 就是位置编码。接下来我们将 $f(...,x_m+p_m,...,x_n+p_n)$ 在 m,n 两个位置上做泰勒展开:
$$\begin{equation}\tilde{f}\approx f + \boldsymbol{p}_m^{\top} \frac{\partial f}{\partial \boldsymbol{x}_m} + \boldsymbol{p}_n^{\top} \frac{\partial f}{\partial \boldsymbol{x}_n} + \frac{1}{2}\boldsymbol{p}_m^{\top} \frac{\partial^2 f}{\partial \boldsymbol{x}_m^2}\boldsymbol{p}_m + \frac{1}{2}\boldsymbol{p}_n^{\top} \frac{\partial^2 f}{\partial \boldsymbol{x}_n^2}\boldsymbol{p}_n + \underbrace{\boldsymbol{p}_m^{\top} \frac{\partial^2 f}{\partial \boldsymbol{x}_m \partial \boldsymbol{x}_n}\boldsymbol{p}_n}_{\boldsymbol{p}_m^{\top} \boldsymbol{\mathcal{H}} \boldsymbol{p}_n}\end{equation}$$
可以看到第1项与位置无关25项仅依赖单一位置第6项f 分别对 m、n 求偏导)与两个位置有关,所以我们希望第六项($p_m^THp_n$)表达相对位置信息,即求一个函数 g 使得
$$p_m^THp_n = g(m-n)$$
$$
\begin{equation}\tilde{f}\approx f + \boldsymbol{p}_m^{\top} \frac{\partial f}{\partial \boldsymbol{x}_m} + \boldsymbol{p}_n^{\top} \frac{\partial f}{\partial \boldsymbol{x}_n} + \frac{1}{2}\boldsymbol{p}_m^{\top} \frac{\partial^2 f}{\partial \boldsymbol{x}_m^2}\boldsymbol{p}_m + \frac{1}{2}\boldsymbol{p}_n^{\top} \frac{\partial^2 f}{\partial \boldsymbol{x}_n^2}\boldsymbol{p}_n + \underbrace{\boldsymbol{p}_m^{\top} \frac{\partial^2 f}{\partial \boldsymbol{x}_m \partial \boldsymbol{x}_n}\boldsymbol{p}_n}_{\boldsymbol{p}_m^{\top} \boldsymbol{\mathcal{H}} \boldsymbol{p}_n}\end{equation}
$$
可以看到第1项与位置无关25项仅依赖单一位置第6项f 分别对 m、n 求偏导)与两个位置有关,所以我们希望第六项($p_m^THp_n$)表达相对位置信息,即求一个函数 g 使得:
$$
p_m^THp_n = g(m-n)
$$
我们假设 $H$ 是一个单位矩阵,则:
$$p_m^THp_n = p_m^Tp_n = \langle\boldsymbol{p}_m, \boldsymbol{p}_n\rangle = g(m-n)$$
$$
p_m^THp_n = p_m^Tp_n = \langle\boldsymbol{p}_m, \boldsymbol{p}_n\rangle = g(m-n)
$$
通过将向量 [x,y] 视为复数 x+yi基于复数的运算法则构建方程:
$$\begin{equation}\langle\boldsymbol{p}_m, \boldsymbol{p}_n\rangle = \text{Re}[\boldsymbol{p}_m \boldsymbol{p}_n^*]\end{equation}$$
$$
\begin{equation}\langle\boldsymbol{p}_m, \boldsymbol{p}_n\rangle = \text{Re}[\boldsymbol{p}_m \boldsymbol{p}_n^*]\end{equation}
$$
再假设存在复数 $q_{m-n}$ 使得:
$$\begin{equation}\boldsymbol{p}_m \boldsymbol{p}_n^* = \boldsymbol{q}_{m-n}\end{equation}$$
$$
\begin{equation}\boldsymbol{p}_m \boldsymbol{p}_n^* = \boldsymbol{q}_{m-n}\end{equation}
$$
使用复数的指数形式求解这个方程,得到二维情形下位置编码的解:
$$\begin{equation}\boldsymbol{p}_m = e^{\text{i}m\theta}\quad\Leftrightarrow\quad \boldsymbol{p}_m=\begin{pmatrix}\cos m\theta \\ \sin m\theta\end{pmatrix}\end{equation}$$
$$
\begin{equation}\boldsymbol{p}_m = e^{\text{i}m\theta}\quad\Leftrightarrow\quad \boldsymbol{p}_m=\begin{pmatrix}\cos m\theta \\ \sin m\theta\end{pmatrix}\end{equation}
$$
由于内积满足线性叠加性,所以更高维的偶数维位置编码,我们可以表示为多个二维位置编码的组合:
$$\begin{equation}\boldsymbol{p}_m = \begin{pmatrix}e^{\text{i}m\theta_0} \\ e^{\text{i}m\theta_1} \\ \vdots \\ e^{\text{i}m\theta_{d/2-1}}\end{pmatrix}\quad\Leftrightarrow\quad \boldsymbol{p}_m=\begin{pmatrix}\cos m\theta_0 \\ \sin m\theta_0 \\ \cos m\theta_1 \\ \sin m\theta_1 \\ \vdots \\ \cos m\theta_{d/2-1} \\ \sin m\theta_{d/2-1} \end{pmatrix}\end{equation}
$$
\begin{equation}\boldsymbol{p}_m = \begin{pmatrix}e^{\text{i}m\theta_0} \\ e^{\text{i}m\theta_1} \\ \vdots \\ e^{\text{i}m\theta_{d/2-1}}\end{pmatrix}\quad\Leftrightarrow\quad \boldsymbol{p}_m=\begin{pmatrix}\cos m\theta_0 \\ \sin m\theta_0 \\ \cos m\theta_1 \\ \sin m\theta_1 \\ \vdots \\ \cos m\theta_{d/2-1} \\ \sin m\theta_{d/2-1} \end{pmatrix}\end{equation}
$$
再取 $\theta_i = 10000^{-2i/d}$(该形式可以使得随着|mn|的增大⟨pm,pn⟩有着趋于零的趋势这一点可以通过对位置编码做积分来证明而 base 取为 10000 是实验结果),就得到了上文的编码方式。
当 $H$ 不是一个单位矩阵时,因为模型的 Embedding 层所形成的 d 维向量之间任意两个维度的相关性比较小,满足一定的解耦性,我们可以将其视作对角矩阵,那么使用上述编码:
$$\begin{equation}\boldsymbol{p}_m^{\top} \boldsymbol{\mathcal{H}} \boldsymbol{p}_n=\sum_{i=1}^{d/2} \boldsymbol{\mathcal{H}}_{2i,2i} \cos m\theta_i \cos n\theta_i + \boldsymbol{\mathcal{H}}_{2i+1,2i+1} \sin m\theta_i \sin n\theta_i\end{equation}
$$
\begin{equation}\boldsymbol{p}_m^{\top} \boldsymbol{\mathcal{H}} \boldsymbol{p}_n=\sum_{i=1}^{d/2} \boldsymbol{\mathcal{H}}_{2i,2i} \cos m\theta_i \cos n\theta_i + \boldsymbol{\mathcal{H}}_{2i+1,2i+1} \sin m\theta_i \sin n\theta_i\end{equation}
$$
通过积化和差:
$$\begin{equation}\sum_{i=1}^{d/2} \frac{1}{2}\left(\boldsymbol{\mathcal{H}}_{2i,2i} + \boldsymbol{\mathcal{H}}_{2i+1,2i+1}\right) \cos (m-n)\theta_i + \frac{1}{2}\left(\boldsymbol{\mathcal{H}}_{2i,2i} - \boldsymbol{\mathcal{H}}_{2i+1,2i+1}\right) \cos (m+n)\theta_i \end{equation}
$$
\begin{equation}\sum_{i=1}^{d/2} \frac{1}{2}\left(\boldsymbol{\mathcal{H}}_{2i,2i} + \boldsymbol{\mathcal{H}}_{2i+1,2i+1}\right) \cos (m-n)\theta_i + \frac{1}{2}\left(\boldsymbol{\mathcal{H}}_{2i,2i} - \boldsymbol{\mathcal{H}}_{2i+1,2i+1}\right) \cos (m+n)\theta_i \end{equation}
$$
说明该编码仍然可以表示相对位置。
@@ -730,7 +782,6 @@ class PositionalEncoding(nn.Module):
基于之前所实现过的组件,我们实现完整的 Transformer 模型:
```python
class Transformer(nn.Module):
'''整体模型'''
def __init__(self, args):