- SignalDesk2 hr ago
学习目标 :能记住Transformer的完整流程 → 了解最重要的一些特点(QKV, Mask, Embedding, FFN参数) → 手撕MHA+FFN+位置编码 → 了解近几年有关Transformer的重大改进 难度: 重要性: 古老而又强大的架构… 掌握了他才有资格进阶更高阶的技术… 图片来自gpt制作 整体流程: 得到 tokens: 通过 Tokenizer。一般自然输入就是一段文字,若干个连续的字会被编码为一个Token ID。 例如 I love machine learning ,可能就是被编码为 [1, 55, 101, 33] 4个Token ID。此时维度为 [B, N] , B 表示一次输入多少条文本, N 表示经过 Tokenizer 后的 Token 数量。这里的维度就是 [1, 4] 得到 Encoder Input: 得到一系列的 Token ID 后,会传入 Token Embedding 层,Embedding 查表将每一个 ID 映射为 D 维向量,此时维度为 [B, N, D] 。 然后与 Position Embedding 得到的向量进行相加,维度保持不变。但是引入了新的问题: 1. Position Embedding 是什么,为什么要用? 2.为什么要直接相加,而不是拼接或者其他操作,相加后信息不可逆怎么办?具体查看 Position Embedding 章节 送入 MHA 层: input维度是 X\in\mathbb{R}^{B\times N\times D} ,经过三个独立的线性层,将 X 映射为 Q, K, V 矩阵。 Q=XW_Q,\qquad K=XW_K,\qquad V=XW_V 接着分成 H 个Head,这里一般 H = 8, D = 512 ,此时 Q, K, V 维度为 [B, 8, N, 64] ,接着在单 Head 中计算各自的 Attention Output, Output = \operatorname{Softmax}(\frac{QK^T}{\sqrt{d_k}})V 此时的 output 维度为 [B, 8, N, 64] ,再 Concat 变换回去为 [B, N, 512] ,最后再经过一个线性层 W_O 进行下一层。这里的主要问题就有很多了,包括 Q,K,V 的作用,以及他们的公式等等,都会在下面的章节详细展开。 送入 Add & Norm 层: 主要包含两个操作,Add 和 Norm,其中 Add 就是典型的残差连接。假设 MHA 的输入为 X ,经过 MHA 后得到 A ,其中 X,A \in \mathbb{R}^{B\times N\times D} ,那么 Add 就是: A' = A + X 。 Norm 操作就是 Layer Normalization,作用是对特征进行归一化,并通过可学习参数调整归一化后的分布,帮助模型训练。至于为什么要用 LayerNorm 而不是其他的,看后文解释。 送入 FFN 层: 这一部分看起来很简单,就是两个线性层 + 一个激活函数,用公式表达就是 \operatorname{FFN}(X) =\operatorname{ReLU}(XW_1+b_1)W_2+b_2 X 为输入。其完整的维度变化一般是: [B,N,512]\rightarrow[B,N,2048] \rightarrow[B,N,512] 中间有一个先升维,后降维的过程,这个过程就是 FFN 的核心。MHA 负责跨 Token 的信息交互,FFN 负责对每个 Token 已经聚合的特征进行非线性加工,每个 Token 独立变换特征。 再经过 Add & Norm 层,然后得到的结果当做 K, V (实际上是 Hidden States )送入到 Decoder 中的 cross-attention。 这里直接进入了 Decoder,Decoder其实没什么好说的,架构上和 Encoder 差不太多。其中的 Masked Self-Attention 在后续 MHA 章节有详细说明(Causal Mask),因为这要求 Decoder 在训练的时候不能偷偷看后面的答案。 训练时,Decoder 输入的是右移后的完整目标 Token 序列,利用 Causal Mask 一次性并行计算各个位置的预测。推理时,一个 Token 接着一个 Token 自回归生成。 Decoder 最后的 Hidden States 还要通过 Linear 投影到词表维度 V ,就能预测下一个 Token 了。 但是!!原来的 Transformer 用两端架构,是作为翻译任务,源任务进入 Encoder 为新生成的 Token 提供信息。但是现在的语言模型是 Decoder-Only 架构,他们把输入的 prompt 直接扔到 Decoder 里面,让他直接预测下一个 Token 即可。此时的 cross-attention 那一块直接移除(所以结构上会和 Encoder 几乎一致了)。 至此流程结束了… 但是真正精华,高频考点的地方在下面… Token Embedding 假设模型的词表大小为 V = 50000 ,每个词的隐藏维度是 D = 512 。 那么Embedding 层会维护一个矩阵 W_E\in\mathbb{R}^{50000\times512} 也就是说,词表中有 50000 个 Token,每个 Token 都对应一个长度为 512 的可学习向量。那么对于每一个token id,就会从这里取一个向量出来。 怎么得到的 W_E 矩阵? Embedding 矩阵最开始通常是 随机初始化 的,在训练过程中被逐渐更新的。 Position Embedding 位置编码,顾名思义为模型提供有关“位置”的信息,即当前单词在句子中的位置在哪里。 为什么要用 Position Embedding? 关系到后面的 MHA 的输出,如果不用的话,对于同一个 token 来说,他经过 Self-Attention 后得到的输出在不同位置上是完全相同的。这样就意味着模型处理到后面,无法分辨 A like B 和 B like A 中的 A 的信息量,两个句子的整体输出张量排列不同,但 同一个 Token 得到的上下文表示实际上相同 。 所以我们需要用和“位置”相关的信息辅助模型可以获取知道,在不同位置的 A 的信息是不完全相同的。 Position Embedding 如何实现? 位置编码有很多种实现方式,这里先讲原始 Transformer 使用的 Sinusoidal Positional Encoding(正弦位置编码)。对于正弦位置编码,是直接加在输入的每一个 Token 的,即最后Encoder的输入 z_i = x_i + PE_i 原始 Transformer 使用下面两个公式。 PE(pos,2i)=\sin\left(\frac{pos}{10000^{2i/D}}\right) PE(pos,2i+1)=\cos\left(\frac{pos}{10000^{2i/D}}\right) 其中, pos 表示 Token 在序列中的位置,通常从 0 开始; i 表示特征维度的索引, D 是 Embedding 隐藏向量的维度。这两个公式看起来比较复杂,但实际上只是通过不同频率的正弦和余弦函数,生成一个位置向量。例如,当 D = 512 时, 对于第 0 个位置: PE(0) = [0, 1,0,1,0,1,......] 对于第 1 个位置: PE(1)=[\sin(1),\cos(1),\sin(1/10000^{2/512}),\cos(1/10000^{2/512}),\ldots] 为什么要用sin / cos? 不同位置能够得到不同的编码 模型能够利用编码推断相对位置关系,因为三角函数的性质,对于一个位置为 pos+k 的位置编码,能够通过位置 pos 的正弦和余弦编码进行线性变换得到。这样有助于模型的学习,以及能够直接计算训练时未出现过的位置(对于 long text 有一定的好处)。 现在的位置编码一般是RoPE,具体原理在文章最下面会进行一定的讲解。 为什么可以直接和 Token Embedding 相加? 这里有几个问题: Token Embedding 表示 Token 的特征,而 Position Embedding 表示位置特征,相加起来是何意味?为什么不是两个特征一起送入? 就算相加了,那么对于一个数字,可以有无数种被凑的方式。也就是说加法是不可逆还原的,那么对于一个已经加过的数,模型是怎么知道有多少是表示 Token Embedding ,有多少是表示 Position Embedding。(例如 2 + 5 = 7,但是模型只能看到 7,他并不知道是 -1 + 8 还是 -100 + 107这样…) 原因是: 如果直接拼接,后续参数量会扩大。 直接拼接相当于这里的隐藏维度 2,那么到后续FFN,MHA里面,核心参数量就要 4,这几乎等于整个 Transformer 模型的参数量翻了4倍 这两种 Embedding 或许可以学习到正交的表示。 Insights into GPT-2’s positional encodings 这篇分析文章指出,GPT-2 学习到的 Token Embedding 与 Position Embedding 大致分布在相互正交的子空间中。 相加有一个好处,让语义和位置直接交互。对于初始的 Transformer 输入是 z_i=x_i+p_i 代入到后续的 Self-Attention 后, \begin{aligned} q_i k_j^\top ={}& \underbrace{x_iW_QW_K^\top x_j^\top}_{\text{语义与语义}}\\ &+\underbrace{x_iW_QW_K^\top p_j^\top}_{\text{语义与位置}}\\ &+\underbrace{p_iW_QW_K^\top x_j^\top}_{\text{位置与语义}}\\ &+\underbrace{p_iW_QW_K^\top p_j^\top}_{\text{位置与位置}} \end{aligned} 也就是说相加后可以让 Attention 能够同时考虑内容匹配、位置匹配,以及内容和位置之间的交互 实际上,对于初始的 Transformer 来说,因为输入是 z_i=x_i+p_i 而且, p_i 是可以已知的,所以其实是可逆的。但是根据上面也可以知道,其实模型并不需要逆回去看,他自己完全可以学习到。 实际上也有论文研究过融合差异,对于不同任务结果不尽相同,感兴趣的可以查看 Fusion Matters: Length-Aware Analysis of Positional-Encoding Fusion in Transformers 和 Rethinking Position Embedding Methods in the Transformer Architecture MHA (Multi-Head Attention) 这里讨论更一般的 MHA,即带有 Mask 的 MHA,完整公式为: \operatorname{Softmax} \left( \frac{QK^\top}{\sqrt{d_k}}+M \right) V 其中 Mask 分为两类: Padding Mask: 假设实际只有三个 Token,但为了凑齐 Batch 长度,后面补了一个 PAD。那么不希望 Token 从 PAD 中获得信息,这时候就应该在 Key 维度上对应 PAD 的信息全部抹掉。 Causal Mask: Decoder 中需要使用,它要求第 i 个位置只能关注自己和前面的位置,不能提前看到未来 Token(不然训练的时候就泄题了)。 class MultiHeadAttention(nn.Module): def __init__(self, d_model=512, num_heads=8): super().__init__() assert d_model % num_heads == 0 self.H = num_heads self.d = d_model // num_heads self.wq = nn.Linear(d_model, d_model) self.wk = nn.Linear(d_model, d_model) self.wv = nn.Linear(d_model, d_model) self.wo = nn.Linear(d_model, d_model) def split_heads(self, x): B, N, D = x.shape x = x.reshape(B, N, self.H, self.d) return x.transpose(1, 2) def forward(self, x, valid_keys=None, causal=False): B, N, D = x.shape # 1. QKV Linear + Split Heads q = self.split_heads(self.wq(x)) k = self.split_heads(self.wk(x)) v = self.split_heads(self.wv(x)) # q, k, v: [B, H, N, d] # 2. Attention Scores scores = q @ k.transpose(-2, -1) scores = scores / math.sqrt(self.d) # scores: [B, H, N, N] # 3. Padding Mask # valid_keys: [B, N], True 表示有效 Token if valid_keys is not None: mask = val
- 情报分类:商业与市场研究
- 分类依据:内容涉及商业、投资或市场动态
- 信息来源:服务器 / LINUX DO - 最新话题
- 发布时间:2026/10/11 22:40:06
- No replies yet