深度学习进阶(三)Transformer Block
<p>在上一篇我们已经完成了多头自注意力机制的内容,并知道了它是 Transformer Block 中的一个子模块。</p><p>Transformer Block 是 Transformer 模型的核心计算单元,它不仅创造并应用了多头自注意力机制,还结合了残差学习、归一化等多门技术。</p>
<p><strong>先简单概括一下 Transformer Block 的整体传播如下:</strong></p>
<p></p><div class="math display">\[\mathbf{X} \rightarrow \mathrm{Multi\text{-}Head\ Attention} \rightarrow \mathrm{Add\ \&\ Norm} \rightarrow \mathrm{FFN} \rightarrow \mathrm{Add\ \&\ Norm} \rightarrow \mathbf{Y}
\]</div><p></p><p><img src="https://img2024.cnblogs.com/blog/3708248/202604/3708248-20260404165927789-605199879.png" alt="image.png" loading="lazy"></p>
<p>接下来我们逐步拆解这个过程。</p>
<h1 id="1位置编码positional-encoding-pe">1.位置编码(Positional Encoding, PE)</h1>
<p>在开始 Transformer Block 之前,首先要说明的是它的输入细节。<br>
在前面的自注意力机制中,我们了解了自注意力相对 RNN 实现了信息关联间的全局建模、并行建模。</p>
<p>但实际上,其并非十全十美,一个关键的问题在于:<strong>自注意力本身并不能隐式建模序列的顺序信息。</strong></p>
<p>回想 RNN ,它的计算是按时间步递归的:</p>
<p></p><div class="math display">\[\mathbf{h}_t = f(\mathbf{x}_t, \mathbf{h}_{t-1})
\]</div><p></p><p>关键点在于信息是<strong>按时间顺序一层层传递的</strong>,这意味着输入顺序不能打乱,也就是说模型本身就“知道”谁在前、谁在后,顺序是通过计算路径自然体现的。</p>
<p>而Transformer 的 Attention 是这样的:</p>
<p></p><div class="math display">\[\mathrm{Attention}(Q,K,V)=\mathrm{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V
\]</div><p></p><p>所有 token 同时参与计算,没有“先后处理顺序”的结构约束。<br>
<strong>也就是说,在原始的词嵌入逻辑中,我们虽然可以通过注意力计算得到 token 间的相关性,但这个相关性里是不包括顺序成分的。</strong></p>
<blockquote>
<p><strong>除非我们想办法把一个序列中的顺序信息也加入每个 token 的词向量中。</strong></p>
</blockquote>
<p>这就是位置编码的由来。<br>
于是,Transformer 原论文提出了一种位置编码机制,将位置信息显式注入到输入表示中。<br>
其具体做法是:对每个 token 的词向量 <span class="math inline">\(\mathbf{E}_{embedding}\)</span>,加入一个与其位置相关的向量 <span class="math inline">\(\mathbf{E}_{pos}\)</span>,从而得到最终输入表示:</p>
<p></p><div class="math display">\[\mathbf{X}_{input} = \mathbf{E}_{embedding} + \mathbf{E}_{pos}
\]</div><p></p><p>其中 <span class="math inline">\(\mathbf{E}_{pos}\)</span> 即为位置编码。<br>
原文中设计了一种<strong>基于正弦和余弦函数的固定位置编码方法</strong>如下:<br>
对于位置 <span class="math inline">\(pos\)</span> 和维度索引 <span class="math inline">\(i\)</span>,其定义如下:</p>
<p></p><div class="math display">\[PE(pos, 2i) = \sin\left(\frac{pos}{10000^{2i/d}}\right)
\]</div><p></p><p></p><div class="math display">\[PE(pos, 2i+1) = \cos\left(\frac{pos}{10000^{2i/d}}\right)
\]</div><p></p><p>其中:</p>
<ol>
<li><span class="math inline">\(pos\)</span> 表示 token 在序列中的位置(从 0 开始)。</li>
<li><span class="math inline">\(d\)</span> 表示 embedding 的维度。</li>
<li><span class="math inline">\(i\)</span> 表示维度索引。</li>
</ol>
<p>公式看起来略显复杂,我们举个例子,假定信息如下:</p>
<ol>
<li>序列:我 / 爱 / 你</li>
<li>位置:<span class="math inline">\(pos = 0, 1, 2\)</span></li>
<li>维度:<span class="math inline">\(d = 3\)</span></li>
</ol>
<p>注意,实际实现中词向量维度通常为偶数,这里简化来演示,计算过程如下:</p>
<table>
<thead>
<tr>
<th>位置 pos</th>
<th>token</th>
<th>维度</th>
<th>计算公式</th>
<th>数值</th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td>我</td>
<td>PE₀</td>
<td><span class="math inline">\(\sin\left(\frac{0}{10000^{2\cdot 0/3}}\right)\)</span></td>
<td><span class="math inline">\(0\)</span></td>
</tr>
<tr>
<td>0</td>
<td>我</td>
<td>PE₁</td>
<td><span class="math inline">\(\cos\left(\frac{0}{10000^{2\cdot 0/3}}\right)\)</span></td>
<td><span class="math inline">\(1\)</span></td>
</tr>
<tr>
<td>0</td>
<td>我</td>
<td>PE₂</td>
<td><span class="math inline">\(\sin\left(\frac{0}{10000^{2\cdot 1/3}}\right)\)</span></td>
<td><span class="math inline">\(0\)</span></td>
</tr>
<tr>
<td>1</td>
<td>爱</td>
<td>PE₀</td>
<td><span class="math inline">\(\sin\left(\frac{1}{10000^{2\cdot 0/3}}\right)\)</span></td>
<td><span class="math inline">\(\sin(1) \approx 0.84\)</span></td>
</tr>
<tr>
<td>1</td>
<td>爱</td>
<td>PE₁</td>
<td><span class="math inline">\(\cos\left(\frac{1}{10000^{2\cdot 0/3}}\right)\)</span></td>
<td><span class="math inline">\(\cos(1) \approx 0.54\)</span></td>
</tr>
<tr>
<td>1</td>
<td>爱</td>
<td>PE₂</td>
<td><span class="math inline">\(\sin\left(\frac{1}{10000^{2\cdot 1/3}}\right)\)</span></td>
<td><span class="math inline">\(\approx 0.00215\)</span></td>
</tr>
<tr>
<td>2</td>
<td>你</td>
<td>PE₀</td>
<td><span class="math inline">\(\sin\left(\frac{2}{10000^{2\cdot 0/3}}\right)\)</span></td>
<td><span class="math inline">\(\sin(2) \approx 0.91\)</span></td>
</tr>
<tr>
<td>2</td>
<td>你</td>
<td>PE₁</td>
<td><span class="math inline">\(\cos\left(\frac{2}{10000^{2\cdot 0/3}}\right)\)</span></td>
<td><span class="math inline">\(\cos(2) \approx -0.42\)</span></td>
</tr>
<tr>
<td>2</td>
<td>你</td>
<td>PE₂</td>
<td><span class="math inline">\(\sin\left(\frac{2}{10000^{2\cdot 1/3}}\right)\)</span></td>
<td><span class="math inline">\(\approx 0.00431\)</span></td>
</tr>
</tbody>
</table>
<p>最终得到三个 token 的 PE :</p>
<p></p><div class="math display">\[\mathbf{E}_{pos}(0) =
\]</div><p></p><p></p><div class="math display">\[\mathbf{E}_{pos}(1) \approx
\]</div><p></p><p></p><div class="math display">\[\mathbf{E}_{pos}(2) \approx
\]</div><p></p><p>现在,我们再来理解一下这种设计思路,这其实涉及一些信号处理的知识:</p>
<p>从本质上看,正弦和余弦函数可以被视为一种<strong>周期信号</strong>。当我们用不同频率的正弦/余弦函数去对同一个位置进行编码时,相当于在多个“频率通道”上对该位置进行投影。<br>
而不同频率就是通过指数缩放项 <span class="math inline">\(10000^{2i/d}\)</span> 来实现的,而同时使用正弦和余弦也能避免不同的频率可能得到相同的值的情况。</p>
<p>而且,<strong>不同位置之间的“距离信息”,可以在这些周期函数的相位差中体现出来。</strong> 这样,位置之间的相对关系就可以被隐式表达。<br>
<img src="https://img2024.cnblogs.com/blog/3708248/202604/3708248-20260404165754140-1085467674.png" alt="image.png" loading="lazy"></p>
<p>通过 PE 的设计,我们就完善了对输入数据的顺序信息的建模。<br>
在更近的研究中,对 PE 的获取方法也在不断地创新发展,一个很容易想到的方向就是可学习位置编码,我们之后再详细展开。</p>
<h1 id="2-attention-块">2. Attention 块</h1>
<h2 id="21-多头自注意力子层">2.1 多头自注意力子层</h2>
<p>对于经过嵌入层、注入 PE 后的序列信息 <span class="math inline">\(\mathbf{X}\)</span> (向量化),它在 Transformer Block 中的<strong>第一步</strong>就是应用多头注意力机制进行信息增强。<br>
这一部分我们已经详细展开过,其作用是:<strong>在序列内部建模任意位置之间的依赖关系,实现全局信息交互。</strong></p>
<p>本次的输入与输出保持维度一致,如下:</p>
<p></p><div class="math display">\[\mathbf{X} \in \mathbb{R}^{n \times d} \rightarrow \mathbf{Z} \in \mathbb{R}^{n \times d}
\]</div><p></p><p>这种设计方便了下面的残差学习。</p>
<h2 id="22-残差连接">2.2 残差连接</h2>
<p><strong>Attention 块到这里并没有结束,Transformer Block 在每个子层还增加了残差连接和归一化的步骤。</strong><br>
展开来说,首先对于注意力子层的输出 <span class="math inline">\(\mathbf{Z}\)</span> ,我们直接把它和输入 <span class="math inline">\(\mathbf{X}\)</span> 相加:</p>
<p></p><div class="math display">\[\mathbf{Z}+\mathbf{X}
\]</div><p></p><p>这涉及到残差学习的基本原理,我们在很早之前有所介绍:残差网络<br>
<strong>再简单概述一下:我们知道 <span class="math inline">\(\mathbf{Z}\)</span> 是由可学习的参数决定的,而这种对输出的建模方式会让学习目标从 “直接学习从输入到输出的映射” 转变为 “学习在输入基础上的改变量”,从而改善梯度现象,支撑更深层的网络。</strong></p>
<h2 id="23-layernorm">2.3 LayerNorm</h2>
<p>然后,下一步处理就是归一化:</p>
<p></p><div class="math display">\[\mathbf{X}_{norm} = \mathrm{LayerNorm}(\mathbf{Z}+\mathbf{X})
\]</div><p></p><p>同样在前面的吴恩达深度学习内容中,我们介绍过归一化的基本内容,并在此基础上拓展了Batch Norm。</p>
<p><strong>而这里的 LayerNorm 和 Batch Norm 其实只是数据的选择方向不同。</strong><br>
公式如下:</p>
<p></p><div class="math display">\[\mathrm{LN}(\mathbf{x}) = \frac{\mathbf{x} - \mu}{\sigma} \cdot \gamma + \beta
\]</div><p></p><p>其中:</p>
<ul>
<li><span class="math inline">\(\mu\)</span> 是均值</li>
<li><span class="math inline">\(\sigma\)</span> 是标准差</li>
<li><span class="math inline">\(\gamma, \beta\)</span> 为可学习参数</li>
</ul>
<p>显然,不同于 Batch Norm 对一批次样本的逐个特征做归一化,<strong>LayerNorm 是对每个样本自身的所有特征做归一化。</strong></p>
<p>来简单举个例子:</p>
<p>假设我们有如下特征向量(设 <span class="math inline">\(d=4\)</span>):</p>
<p></p><div class="math display">\[\mathbf{x} =
\]</div><p></p><p>计算均值和标准差如下:</p>
<p></p><div class="math display">\[\mu = \frac{2 + 4 + 6 + 8}{4} = 5
\]</div><p></p><p></p><div class="math display">\[\sigma = \sqrt{\frac{(2-5)^2 + (4-5)^2 + (6-5)^2 + (8-5)^2}{4}} = \sqrt{5}
\]</div><p></p><p>代入计算:</p>
<p></p><div class="math display">\[\hat{\mathbf{x}} = \left[\frac{2-5}{\sqrt{5}}, \frac{4-5}{\sqrt{5}}, \frac{6-5}{\sqrt{5}},\frac{8-5}{\sqrt{5}}\right]
\]</div><p></p><p>现在,该向量均值为 <span class="math inline">\(0\)</span>,方差为 <span class="math inline">\(1\)</span> 。<br>
接下来再通过学习平移和缩放参数 <span class="math inline">\(\gamma, \beta\)</span>:</p>
<p></p><div class="math display">\[\mathbf{y} = \hat{\mathbf{x}} \cdot \gamma + \beta
\]</div><p></p><p>这一步的作用是:<strong>在完成标准化之后,让模型再特化学习“什么样的分布是最合适的”。</strong></p>
<p>可以这样理解 LayerNorm 的作用: <strong>它可以保持特征的相对结构,同时将整体数值尺度归一到稳定范围。</strong><br>
这样做使得每一个 token 的表示都被拉回到一个稳定的数值范围,从而避免在深层网络中出现数值分布不断偏移的问题。</p>
<p>你可能还有这样一个问题:</p>
<blockquote>
<p><strong>为什么不用 Batch Norm ?</strong></p>
</blockquote>
<p>这便是针对问题的特化所在:<br>
<strong>在这里,最重要的原因是注意力计算和词嵌入本身都会导致每个 token 的分布都不同,用 BatchNorm 强行对齐反而会破坏信息。</strong></p>
<p>同样举个简单例子来说明:</p>
<p>假设有一句话:“猫 吃 鱼”,则三个 token 经过注意力后可能会出现这种情况:</p>
<ol>
<li>猫关注“吃”:<span class="math inline">\(\mathbf{z}_1 = \)</span> ,比较平缓。</li>
<li>吃关注“猫”和“鱼”:<span class="math inline">\(\mathbf{z}_2 = \)</span> ,某一维特别大。</li>
<li>鱼关注“吃”:<span class="math inline">\(\mathbf{z}_3 = [-3, -2, -1, 0]\)</span>,整体偏负。</li>
</ol>
<p>也就是说:<strong>每个 token 的“特征分布”完全不同。</strong></p>
<p>而这就是关键点,<strong>BatchNorm 的假设是:不同样本的同一维度,应该有类似分布。但在这里不同 token 的分布本来就应该不同!</strong><br>
因为每个 token 表达不同语义,而注意力让它们差异更大。如果用 BatchNorm 强行把它们拉成一样 ,反而破坏语义差异。<br>
此外,序列数据的长度不固定、BatchNorm 依赖批次大小等因素也会产生负面影响。</p>
<p>这便是 Attention 块 的全部逻辑,写成一个总体公式如下:</p>
<p></p><div class="math display">\[\mathbf{X}_{out} = \mathrm{LayerNorm}\left(\mathbf{X} + \mathrm{MultiHead}(\mathbf{X})\right)
\]</div><p></p><p><img src="https://img2024.cnblogs.com/blog/3708248/202604/3708248-20260404165503184-1857432040.png" alt="image.png" loading="lazy"></p>
<h1 id="3-前馈神经网络-ffn">3. 前馈神经网络 FFN</h1>
<p>经过Attention 块完成信息增强后,下一步就是就是输入一个小型前馈网络进行计算,这里提供的就是<strong>非线性变换、重构特征</strong>的逻辑。</p>
<p>形式上,FFN 通常由两层线性变换加一个激活函数构成:</p>
<p></p><div class="math display">\[\mathrm{FFN}(\mathbf{X}) = \max(0, \mathbf{X}\mathbf{W}_1 + \mathbf{b}_1)\mathbf{W}_2 + \mathbf{b}_2
\]</div><p></p><p>其中的维度设计较为关键:</p>
<ol>
<li><span class="math inline">\(\mathbf{W}_1 \in \mathbb{R}^{d \times d_{ff}}\)</span>、 <span class="math inline">\(\mathbf{W}_2 \in \mathbb{R}^{d_{ff} \times d}\)</span>。</li>
<li><span class="math inline">\(d_{ff}\)</span> 通常远大于 <span class="math inline">\(d\)</span>,在原文中设计为 <span class="math inline">\(4d\)</span> 。</li>
</ol>
<p>其具体传播过程如下:<br>
<img src="https://img2024.cnblogs.com/blog/3708248/202604/3708248-20260404165501481-1023043394.png" alt="image.png" loading="lazy"><br>
<strong>在这里,你会发现 FFN 在序列间其实没有任何信息交互,其变换、建模完全局限于单个 token,因此必须与注意力机制配合使用。</strong></p>
<p>同样地,在完成 FFN 传播得到输出后,也要进行残差连接和归一化。</p>
<p></p><div class="math display">\[\mathbf{Y} = \mathrm{LayerNorm}(\mathbf{X}_{out} + \mathrm{FFN}(\mathbf{X}_{out}))
\]</div><p></p><p>至此,就完成了 Transformer Block 的全部计算,在实际模型中,这样的 Block 会被堆叠多层,从而逐步提升模型对复杂结构和长距离依赖的建模能力。</p>
<h1 id="4post-norm-和-pre-norm">4.Post-Norm 和 Pre-Norm</h1>
<p>在这里值得补充的一点关于 Post-Norm 和 Pre-Norm,我们先把 Transformer Block 的两个核心公式再摆一遍:</p>
<p></p><div class="math display">\[\mathbf{X}_{out} = \mathrm{LayerNorm}\left(\mathbf{X} + \mathrm{MultiHead}(\mathbf{X})\right)
\]</div><p></p><p></p><div class="math display">\[\mathbf{Y} = \mathrm{LayerNorm}(\mathbf{X}_{out} + \mathrm{FFN}(\mathbf{X}_{out}))
\]</div><p></p><p>这里,也是原论文的逻辑是:<strong>先计算,再对结果归一化。</strong><br>
这种结构通常被称为 <strong>Post-Norm(后归一化)结构</strong>:也就是每一个子层(Attention 或 FFN)都先完成自身的计算,再与输入进行残差相加,最后统一进行归一化处理。</p>
<p>然而,在后续研究实践中,发现了这种结构在<strong>层数较深时容易出现训练不稳定的问题</strong>,于是提出了另一种更常用的变体:<strong>Pre-Norm(前归一化)结构</strong>。<br>
其对应形式为:</p>
<p></p><div class="math display">\[\mathbf{X}_{out} = \mathbf{X} + \mathrm{MultiHead}(\mathrm{LayerNorm}(\mathbf{X}))
\]</div><p></p><p></p><div class="math display">\[\mathbf{Y} = \mathbf{X}_{out} + \mathrm{FFN}(\mathrm{LayerNorm}(\mathbf{X}_{out}))
\]</div><p></p><p>可以看到,其变化在于:<strong>先对输入进行归一化,再送入子层计算,最后再进行残差连接。</strong></p>
<p>其更稳定的原因仍在梯度传播的逻辑上:<br>
我们设计残差连接,形成恒等映射,让梯度可以绕过子层计算直接传播,从而保证了深层网络中的稳定性。<br>
而在 Post-Norm 中,LayerNorm 位于残差连接后,也就是<strong>梯度传播必须先经过归一化</strong>,这导致其在多层堆叠时更容易发生缩放与扰动,从而影响训练稳定性,相比之下,Pre-Norm 避免了这一问题,保留了直连的残差路径。<br>
<strong>因此在实际工程中,更常见的是 Pre-Norm 结构。</strong></p>
<p>至此,我们已经完整构建了 Transformer 的基本计算单元,在下一篇中,就可以较丝滑地进入 Transformer 的整体结构了。</p><br><br>
来源:https://www.cnblogs.com/Goblinscholar/p/19821451
頁:
[1]