薰衣草恋人 發表於 2025-9-9 22:01:00

Transformer通俗讲解(大白话版)

<blockquote>
<p>温馨提示:本文共有<code>8472</code>个字,平均阅读时间约为<code>34</code>分钟<br>
大家可以快速查看自己感兴趣的内容点击下面的目录:</p>
</blockquote>
<p></p><div class="toc"><div class="toc-container-header">目录</div><ul><li>模型简介</li><li>整体架构<ul><li>Encoder结构<ul><li>输入阶段<ul><li>输入嵌入(Input Embedding)</li><li>位置编码(Position Encoding)</li><li>输入向量构建</li></ul></li><li>Attention结构<ul><li>自注意力机制 Self-Attention 和 缩放点积注意力 Scaled Dot-Product Attention<ul><li>第一步 生成QKV</li><li>第二步 计算注意力分数</li><li>第三步 缩放与Softmax</li><li>第四步 加权得到输出</li></ul></li><li>多头(自)注意力机制 Multi-Head Attention<ul><li>多的“头”是什么</li><li>多头的作用</li></ul></li><li>Encoder结构中的多头注意力的输出</li></ul></li><li>残差连接与层归一化 Add&amp;Normalize<ul><li>残差连接</li><li>层归一化</li></ul></li><li>前馈神经网络 Feed Forward<ul><li>这一项的意义</li></ul></li><li>Encoder的流程与训练</li></ul></li><li>Decoder结构(训练)<ul><li>输入阶段</li><li>目标序列的右移</li><li>掩码多头注意力机制<ul><li>掩码</li><li>掩码类型</li><li>掩码多头注意力的输出</li></ul></li><li>Decoder的第二个多头注意力模块</li><li>Decoder的流程与训练</li><li>输出</li></ul></li><li>Decoder结构(推理)</li></ul></li></ul></div><p></p>
<hr>
<p>​        最近肝<strong>大模型综述和时序transformer</strong>相关的工作太多了,回头看似乎这个最基础的结构似乎还是<strong>有点忘得差不多了</strong>,所以抽出一个下午时间简单地做了一个简单的模型结构拆分,用了我<strong>最通俗的语言</strong>进行一个简单的解释吧。</p>
<h1 id="模型简介">模型简介</h1>
<p>​        作为机器学习领域必读的经典模型,Transformer模型首次提出于<strong>《Atttention is all you need》</strong>这篇论文中。</p>
<p>​        最早应用于NLP领域,作为文字处理任务的一个解决方法。后来被引申应用于图像、时序等领域,并为大语言模型的构建提供了基础。</p>
<blockquote>
<p>灵感来自于传统的“电力变压器”结构(Transformer),不是变形金刚电影(也叫Transformers)</p>
</blockquote>
<h1 id="整体架构">整体架构</h1>
<p><img src="https://img2024.cnblogs.com/blog/2884766/202509/2884766-20250910143413899-884221.png" alt="Transformer整体架构图" loading="lazy"></p>
<p>​        我们首先举一个例子,我们从整体的流程开始走一遍,从这个过程中了解transformer的架构构成。</p>
<p>​        假如我们的输入是“我”,“是”, “人”三个汉字,并且想使用它预测我下一个字,我们将这个输入的一个中文序列作为输入X</p>
<h2 id="encoder结构">Encoder结构</h2>
<p>​        Transformer的第一层架构<strong>Encoder结构</strong>,它的主要任务就是理解我们现在输入的向量内容,并且将其转化为一个中间表示。</p>
<h3 id="输入阶段">输入阶段</h3>
<h4 id="输入嵌入input-embedding">输入嵌入(Input Embedding)</h4>
<p><img src="https://img2024.cnblogs.com/blog/2884766/202509/2884766-20250910143520347-419756014.png" alt="输入嵌入Input Embedding" loading="lazy"></p>
<p>​        首先第一步,模型无法直接读取中文,我们首先要将这个中文序列转换为计算机可以认识的内容,这一步就是输入嵌入(IE)过程。在这个过程中,模型会<strong>使用词嵌入的方式将序列转化到一个向量</strong>。</p>
<p><strong>通俗理解</strong></p>
<blockquote>
<p>transformer翻开词典,找到你输入的词,并且翻译成自己认识的数学语言</p>
</blockquote>
<p><strong>原文理解</strong></p>
<blockquote>
<p>​        在这里使用了一个可学习的嵌入矩阵,大小为(在原文中d_model尺寸为512)</p>
<p>​        每个词被映射为d_model维的向量,这个结果会被乘以<span class="math inline">\(\sqrt{d\_model}\)</span>进行缩放</p>
</blockquote>
<h4 id="位置编码position-encoding">位置编码(Position Encoding)</h4>
<p><img src="https://img2024.cnblogs.com/blog/2884766/202509/2884766-20250910143609973-1393041879.png" alt="位置编码Position Encoding" loading="lazy"></p>
<p>​        众所周知我们现在的中文一般是从左往右进行阅读的,但是对于古人或者外国人(尤其是一些中东国家)来说,他们阅读和书写的顺序可能是从上到下或者从右到左。</p>
<p>​        所以对于模型来说,我们也需要对他<strong>说明这些输入的词语位置</strong>,方便他理解上下文信息。所以这里我们就需要用到位置编码(PE)了。</p>
<p><strong>通俗理解</strong></p>
<blockquote>
<p>​        我们通过标注的方式,给每个词标记了<strong>相对位置</strong>,并且特别标注了<strong>“哪个词与哪个词意思相近”</strong>。比如说我输入了“男”、“树”和“女”,可能这里就把“男”和“女”标注意思相近,方便模型理解。</p>
</blockquote>
<p><strong>原文理解</strong></p>
<blockquote>
<p>​        Transformer结构与RNN不同,不能顺序读取序列,所以需要标注清楚每个词在句子中的位置</p>
<p>​        Transformer的位置编码使用了<strong>固定相对位置编码</strong>,运用了正弦函数和余弦函数共同标注词的位置</p>
<blockquote>
<p>​        <span class="math inline">\(PE_{ (pos, 2i)} = sin(pos / 10000^{(2i/d\_model)})\)</span></p>
</blockquote>
<blockquote>
<p>​        <span class="math inline">\(PE_{ (pos, 2i+1)}   = cos(pos / 10000^{(2i/d\_model)})\)</span></p>
</blockquote>
<blockquote>
<blockquote>
<p>​        其中,pos是表示<strong>词语在句子里面的位置</strong>,i是索引维度(奇偶维度分开),d_model是模型维度</p>
</blockquote>
<p>​        运用正余弦定理,<strong>任意相对位置都可以通过某一位置相加计算得到</strong>。</p>
</blockquote>
</blockquote>
<h4 id="输入向量构建">输入向量构建</h4>
<p>​        经过上述的变化之后,我们的中文序列就转化为了可以被模型认识的向量表示了。最终的输入表示 <strong>X</strong>就由<strong>词嵌入</strong> 和<strong>位置编码</strong> 相加得到。</p>
<h3 id="attention结构">Attention结构</h3>
<p>​        作为整个Transformer结构最为重要的部分之一(从论文名字就能看出来吧),看见他的名字我们就可以看出来,它的主要作用就是<strong>发掘序列中值得模型重点关注</strong>的地方。那么他是如何工作的,我们接下来看一看吧。</p>
<h4 id="自注意力机制-self-attention-和-缩放点积注意力-scaled-dot-product-attention">自注意力机制 Self-Attention 和 缩放点积注意力 Scaled Dot-Product Attention</h4>
<p><img src="https://img2024.cnblogs.com/blog/2884766/202509/2884766-20250910145913611-1858496660.png" alt="缩放点积注意力" loading="lazy"></p>
<blockquote>
<p>PS:这里提到的<strong>缩放点积注意力</strong>是所有注意力机制数学化的基础计算原理(文中说明的),而<strong>自注意力机制</strong>则是其最基础的表现形式。很多人会把它们混用,但在技术严谨性上,它们是<strong>“机制”与“实现”</strong>的关系。</p>
</blockquote>
<p>​        作为最基础的注意力单元,也是模型中所有注意力结构的最基础结构,按照<strong>缩放点积注意力</strong>构建的<strong>自注意力单元</strong>已经成为Transformer中最为独特的创新点。要想理解它的工作原理,我们这里还是拿我们已经经过构建的输入向量<strong>X</strong>做一个比喻吧。</p>
<blockquote>
<p>我们<strong>暂且</strong>认为经过变换后的X仍然表示这<strong>“我”,“是”,“人”</strong>这三个词吧。</p>
</blockquote>
<h5 id="第一步-生成qkv">第一步 生成QKV</h5>
<p>​        用到这个结构的时候,我们需要将我们的输入序列进行拆分,拆分为Q、K、V三个向量。</p>
<ol>
<li>
<p><strong>Q(query)查询</strong>:“我在找什么”。在句子中,当前词想要了解自己与其他词与自己的关系。</p>
</li>
<li>
<p><strong>K(Key)键值</strong>:“我是谁”。在句子中,相当于自己的“标签”,每个词向外界展示的特征,用于匹配查询。</p>
</li>
<li>
<p><strong>V(value)数值</strong>:“我的内容是什么”。在句子中,每个词真正要传达的信息.</p>
</li>
</ol>
<p><strong>通俗理解</strong>:</p>
<blockquote>
<p>​        想象注意力机制就是一个人在翻字典,就比如说输入了“我是人”这个句子,他翻到了“我”这个字:</p>
<p>​        Q就是在想知道“我”可以组成什么词语(或者和哪个词语关系大);</p>
<p>​        K就是每个词的页码;</p>
<p>​        V就是这个每个词在词典中对应的解释。</p>
</blockquote>
<p>​        这样分开式地处理,让QK可以更加专注于词语的匹配,而V只需考虑信息的传递,避免了很多问题(自相关等)。</p>
<p>​        那么我们如何得到这三个主要的向量呢,在这里,输入向量X需要通过3个不同的线性变换矩阵得到Q、K、V,也就是:</p>
<p><code>Q = X·W_Q</code>,<code>K = X·W_K</code>,<code>V = X·W_V</code></p>
<blockquote>
<p>​        这些权重矩阵W_Q、W_K、W_V是模型训练过程中学习得到的</p>
</blockquote>
<h5 id="第二步-计算注意力分数">第二步 计算注意力分数</h5>
<p>​        了解了通过Q、K、V这样一个可以快速计算词与词之间关系的方法,我们就需要通过这一步计算<strong>每个词与每个词之间的关系</strong>,也就是<strong>计算注意力分数</strong>。</p>
<blockquote>
<p>​        在这个阶段,一般是通过将所有词向量拼接为一个大的向量统一进行运算,但是这里方便理解我们就还是将每个词向量进行拆分计算吧。</p>
</blockquote>
<p>​        所以在这一步我们需要计算每个词的Query和所有词Key的相似度:<span class="math inline">\(Q * K^T\)</span>。相当于衡量<strong>"每个词想了解的内容"</strong>(<span class="math inline">\(Q\)</span>)与<strong>"其他词提供的特征"</strong>(<span class="math inline">\(K^T\)</span>)的匹配程度。</p>
<p><strong>通俗理解:</strong></p>
<blockquote>
<p>​        这一步就好理解了。我们将“我是人”的Q * K^T简化为<span class="math inline">\( * ^T\)</span>(T为转置)。</p>
<p>​        <span class="math inline">\(Q * K^T\)</span>这个过程就相当于:</p>
<p>​        注意力机制开始翻字典,当前的“我”字在第22页。他首先翻到了第33页,翻到了“是”这个词,看了看下面的解释,感觉他们语序之间<strong>有关系</strong>,那么这个<span class="math inline">\(Q_1*K_2\)</span>计算结果就赋予一个0.5的相关性。</p>
<p>​        接下来它又翻到了55页,看到了“人”这个词,感觉他们语序之间<strong>有关系但不多</strong>,那么这个<span class="math inline">\(Q_1*K_3\)</span>计算结果就赋予一个0.3的相关性。</p>
<p>​        按照这个顺序,就可以计算其他不同词之间的关系,最后统一得到<span class="math inline">\(Q * K^T\)</span>的结果。也就是<strong>每个词与所有词只见的相似度</strong>。</p>
</blockquote>
<h5 id="第三步-缩放与softmax">第三步 缩放与Softmax</h5>
<p>​        为了方式数值过大,这里使用了一个缩放。使用当前计算分数除以$\sqrt{d_k} $(d_k是Key的维度)防止数值过大。</p>
<blockquote>
<p>​        这里有的文章说可以使用其他的值作为分母进行缩放,本质上都是为了防止内积过大的操作</p>
</blockquote>
<p>​        然后通过softmax将这个分数转化为一个概率分布,所得到的结果就是注意力分布。</p>
<p><strong>通俗理解:</strong></p>
<blockquote>
<p>​        一个不太恰当但浅显的例子,比如说得到的最终分数为1,2,3。那么经过softmax之后就变成了1/6,2/6,3/6,得到了注意力权重就是0.17,0.33,0.5这样子变成一个概率分布。</p>
</blockquote>
<h5 id="第四步-加权得到输出">第四步 加权得到输出</h5>
<p>​        最后使用注意力权重与每个词的V进行加权求和:<span class="math inline">\(Output=Attention\_weight * V\)</span>,这样的输出就可以让我们的向量本身包含<strong>更加丰富的语义信息</strong>。</p>
<p><strong>通俗理解:</strong></p>
<blockquote>
<p>​        就拿上面的“我”这个词来说,比如说它对于“是”和“人”的注意力权重分别是0.5和0.3(现实应该是一个矩阵,这里方便理解化为一维),那么“我”这个词就会<strong>根据注意力权重</strong>有选择地学习到“是”和“人”的语义信息,成为了一个<strong>与“是”和“人”上下文信息的全新的“我”向量</strong>。</p>
</blockquote>
<p><img src="https://img2024.cnblogs.com/blog/2884766/202509/2884766-20250910150527563-727735816.png" alt="attention" loading="lazy"></p>
<p>​        这样一个self-attention结构,就让每个词语可以学习到与自己相关的词语的上下文信息。回头我们再看它的整个公式,我们就可以更加清晰地理解其中的意思。</p>
<h4 id="多头自注意力机制-multi-head-attention">多头(自)注意力机制 Multi-Head Attention</h4>
<p><img src="https://img2024.cnblogs.com/blog/2884766/202509/2884766-20250910150300684-1392593508.png" alt="多头注意力" loading="lazy"></p>
<p>​        在原文结构中,我们可以看到我们的输入序列进入的Encoder结构中遇到的第一个处理就是这个<strong>多头注意力</strong>,那么这个多头注意力和我们上述提到的自注意力机制有什么区别呢?</p>
<p>​        正如我们上文所说的那样,所谓的<strong>多头注意力机制</strong>就是由<strong>多个自注意力合并</strong>而来。</p>
<p>​        那么具体它的结构到底是什么,我们接下来会进行一个更加详细的解释。</p>
<h5 id="多的头是什么">多的“头”是什么</h5>
<p>​        首先相信大家也很好奇这里面所谓的头指的是什么。我们继续将所谓的<strong>自注意力机制</strong>看为一个<strong>翻字典的人</strong>。<br>
<img src="https://img2024.cnblogs.com/blog/2884766/202509/2884766-20250910150324359-1041434670.png" alt="多头" loading="lazy"></p>
<p>​        有的时候我们在解决问题的时候,每个人都有不同的看法,包括我们翻字典这件事情来说也是一样的。有的人认为“我”和“是”关系比较密切,有的人认为“我”和“人”的关系比较密切。</p>
<p>​        所以这个时候,我们就需要不同的人来一起翻字典,来让结果尽可能地“服众”(或者说趋于一个一致的结果)。</p>
<p>​        那么这个时候我们就构成了一个“多头”的概念。</p>
<p><strong>通俗解释:</strong></p>
<blockquote>
<p>​        不同的翻字典的人得到自己对于句子的理解后,汇总起来出一个比较统一的结果。不同的翻字典的人就被视为<strong>“头”</strong>,也就是从不同的角度出发。</p>
<p>​        有的“头”从语言学的角度看“我是人”这个句子。“我”是主语,应该后面连上“是”才能使句子通畅,所以“我”和“是”注意力分数高;</p>
<p>​        有的“头”从生物学的角度看“我是人”这个句子。“我”在生物学标准上属于动物,所以“我”和“人”注意力分数高。</p>
<p>​        ……</p>
<p>​        等所有的“头”处理完之后,将所有的结果<strong>汇总起来</strong>得到最终我们的输入句子<strong>“我是人”</strong>的注意力结果。</p>
</blockquote>
<p><strong>原文解释:</strong></p>
<blockquote>
<p>​        在原文中,一共分为了8个注意力头,每个头独立计算自己的注意力。</p>
<p>​        不同的是,他们独立随机初始化属于自己的QKV权重,并独立计算注意力,使得每个注意力头可以关注不同语义信息。</p>
<p>​        <span class="math inline">\(head_i = Attention(Q·W^Q_i, K·W^K_i, V·W^V_i)\)</span></p>
<blockquote>
<p>​        这里的i指代的对应的头</p>
</blockquote>
<p>​        最终将8个头的64维输出拼接成512维向量,并通过线性变换<strong>调整维度为与输入X相同的维度</strong>。</p>
<p>​        <span class="math inline">\(MultiHead = Concat(head_1,...,head_8)·W^O\)</span></p>
</blockquote>
<h5 id="多头的作用">多头的作用</h5>
<p>​        为什么要将多个自注意力拼接起来使用呢?我觉得有着以下的原因:</p>
<ol>
<li>使得模型可以对Q、K、V不同的向量进行更加细化深入的构建,每个"头"专注一种<strong>特定关系类型</strong>。</li>
<li>多个自注意力的<strong>随机初始化</strong>可以<strong>消除偏差</strong>的影响,让词与词之间的学习更加丰富。</li>
</ol>
<p>原文中对于整个过程的定义为:<br>
<img src="https://img2024.cnblogs.com/blog/2884766/202509/2884766-20250910150608129-1705155562.png" alt="多头注意力" loading="lazy"></p>
<p>和我们分析的一致。</p>
<h4 id="encoder结构中的多头注意力的输出">Encoder结构中的多头注意力的输出</h4>
<p>​        最终,我们得到了一个和输入的向量X大小维度相同的输出变量Z。</p>
<p>​        这个Z变量和输入的X相比,<strong>它不仅包含了句子“我是人”的所有信息,还包含了每个词之间的注意力关系等丰富的语义。</strong></p>
<h3 id="残差连接与层归一化-addnormalize">残差连接与层归一化 Add&amp;Normalize</h3>
<p><img src="https://img2024.cnblogs.com/blog/2884766/202509/2884766-20250910150407967-1157608837.png" alt="Add&amp;Norm" loading="lazy"></p>
<p>​        在Encoder的多头注意力之后,向量进入了一个Add&amp;Normalize层。它由<strong>残差连接</strong>和<strong>层归一化</strong>两部分组成。其实在Encoder结构中,在每次大型操作后(<strong>多头注意力操作、前馈神经网络</strong>)都会进行一次这个层。</p>
<p>​        下面我们用<strong>多头注意力操作后</strong>的Add&amp;Normalize举一个例子,看一看它的具体结构和操作。它的公式也很简单:</p>
<p></p><div class="math display">\[Output = X + Attention(X)
\]</div><p></p><p></p><div class="math display">\[LayerNorm(Output)
\]</div><p></p><h4 id="残差连接">残差连接</h4>
<p>​        根据公式可得,是一个非常简单的过程。</p>
<p></p><div class="math display">\[Output = X + Attention(X)
\]</div><p></p><p>​        简单来说:把多头注意力之前的输入X和经过注意力处理的输出Z相加。</p>
<p>​        那么为什么要这么做呢,我们明明都处理好了,给句子加上了注意力,为什么还要加上没有经过注意力处理的句子呢?</p>
<p><strong>通俗理解:</strong></p>
<blockquote>
<p>​        给句子加一个存档,我们可以通过比对存档让我们知道目前句子经过了哪些修改,防止经过大型操作之后句子的某些信息丢失。</p>
</blockquote>
<p><strong>原文理解:</strong></p>
<blockquote>
<p>​        我们首先要了解:神经网络退化指的是在达到最优网络层数之后,神经网络还在继续训练导致Loss增大。</p>
<p>​        如果没有残差链接,在训练时梯度会越来越小直至饱和,训练也会越来越困难。而有了残差链接后,可以有效解决梯度消失的问题。</p>
<p>​        残差连接后,可以让网络更加专注于存在差异的部分进行训练。</p>
<p>​        同样的,即使子层学习效果不佳,也能保证至少保留原始输入信息。</p>
</blockquote>
<h4 id="层归一化">层归一化</h4>
<p>​        层归一化是一个比较通用的技术,相当于通过全览向量信息之后,将所有向量整理到一个相同的水平上,方便后续操作。</p>
<p></p><div class="math display">\[LayerNorm(Output)
\]</div><p></p><p>​        层归一化本身的公式:<span class="math inline">\(LayerNorm(x) = γ * (x - μ) / √(σ² + ε) + β\)</span>。其中μ是均值,σ²是方差。γ和β是可学习的缩放和平移参数。ε是小常数,防止除零错误。</p>
<p><strong>通俗理解:</strong></p>
<blockquote>
<p>对当前句子“我是人”这句话的字体统一设成“微软雅黑”,22字号。</p>
</blockquote>
<p><strong>原文理解:</strong></p>
<blockquote>
<p>​        这里的归一化,是对于当前样本的所有特征进行归一化。(区别于批归一化:对同一批次的不同样本的同一特征归一化)</p>
<p>​        归一化使优化曲面更平滑,梯度下降更高效。</p>
</blockquote>
<p>​        通过这一层后,所有的词向量变得更加适合模型操作了。</p>
<h3 id="前馈神经网络-feed-forward">前馈神经网络 Feed Forward</h3>
<p><img src="https://img2024.cnblogs.com/blog/2884766/202509/2884766-20250910150834515-1288038313.png" alt="前馈神经网络" loading="lazy"></p>
<p>​        这一层主要是由两层全连接层构成,它的公式很简单:</p>
<p></p><div class="math display">\[FFN(X)=max(0,xW_1+b_1)W_2+b_2
\]</div><p></p><p>​        讲解一下这个网络,X就是上述我们经过多头注意力和Add&amp;Norm的输入词”我是人“的向量表示。它的作用就是将我们的向量转化为一个<strong>更高维度的向量进行语义特征联系</strong>,然后在恢复到原来维度。</p>
<p>​        第一层网络就是一个很简单的线性函数<span class="math inline">\(f(x) = (xW_1+b_1)\)</span>,其中<span class="math inline">\(W_1\)</span>是第一层的权重,它的维度为<span class="math inline">\((d_model×d_ff)\)</span>,一般这个d_ff是d_model的4倍,它的作用就是将我们的词向量映射到更高维度。</p>
<p><strong>通俗解释:</strong></p>
<blockquote>
<p>​        将句子”我是人“的每个词拆解成拼音”wo shi ren“,并且将每个词的笔画拆解,模型觉得他们之间可能存在更多的关系。</p>
</blockquote>
<p>​        然后通过一个ReLU函数,<span class="math inline">\(f(x) = max(0,xW_1+b_1)\)</span> 来进行非线性的引入,将强特征增强,抑制弱特征,去除杂项。</p>
<p>​        最后再回复到原有的维度<span class="math inline">\(f(x)=xW_2+b_2\)</span> ,这里的W_2就是第二层的权重,它的权重就是$(d_ff×d_model) $ 让向量回归正常维度。</p>
<h4 id="这一项的意义">这一项的意义</h4>
<p>​        多头注意力的本质上只是线性的加权,针对语义来说,可能只学习到了基础的上下文关联关系。而<strong>前馈网络项</strong>最为重要的<strong>升维+ReLU</strong>则是给这个向量带来更多<strong>非线性特征</strong>的学习能力。</p>
<p><strong>通俗理解:</strong></p>
<blockquote>
<p>​        通过这一步,我们的模型学习再学习了上下文关系后,还能学到:</p>
<blockquote>
<p>​        情感强度特征</p>
</blockquote>
<blockquote>
<p>​        语义角色特征</p>
</blockquote>
<blockquote>
<p>​        时态特征</p>
</blockquote>
<blockquote>
<p>​        与其他词的复杂关系</p>
</blockquote>
<p>通过ReLU激活,只保留有意义的特征组合</p>
</blockquote>
<h3 id="encoder的流程与训练">Encoder的流程与训练</h3>
<p>​        经过了我们的<strong>多头注意力机制——&gt;Add&amp;Norm——&gt;Feed Forward——&gt;Add&amp;Norm</strong> 这样的一个结构,就构成了我们大名鼎鼎的Encoder结构。<br>
<img src="https://img2024.cnblogs.com/blog/2884766/202509/2884766-20250910150755825-1169739502.png" alt="Encoder整体过程" loading="lazy"></p>
<p>​        在原文的结构中,我们的模型通过了<strong>6层堆叠的Encoder架构</strong>进行学习。也就是说,这一层Encoder结构的输出,会被作为下一层Encoder架构的输入,循环6次。</p>
<p>​        通过这个过程,我们的模型对于”我是人“这个句子的理解到达了”空前的高度“,那么接下来,就需要完成它的预测任务了,根据他的”理解“来生成了。</p>
<h2 id="decoder结构训练">Decoder结构(训练)</h2>
<p>​        有了Encoder结构,那肯定也有Decoder结构。在这个结构中,它的组成部分和Encoder有些类似,但是仍有部分不同:<br>
<img src="https://img2024.cnblogs.com/blog/2884766/202509/2884766-20250910150914156-896356779.png" alt="Decoder" loading="lazy"></p>
<ul>
<li>包含两个多头注意力机制
<ul>
<li>第一个多头注意力使用了<strong>掩码操作</strong>,构成了<strong>掩码多头注意力机制</strong></li>
<li>第二个多头注意力的<strong>Q使用了第一个多头注意力的输出</strong>,而它的<strong>K和V则是使用了Encoder的输出</strong>。</li>
</ul>
</li>
<li>最后使用了Softmax来计算可能词语的概率。</li>
</ul>
<blockquote>
<p>PS:小建议,最好把Encoder和Decoder看作两个<strong>完全独立</strong>的工厂,Encoder看作<strong>原材料工厂</strong>,Decoder看作<strong>加工工厂</strong>。</p>
</blockquote>
<p>​        接下来我们继续按照Encoder的分析方式,从输入到输出分析一下Decoder的工作原理。</p>
<h3 id="输入阶段-1">输入阶段</h3>
<p>​        相较于Encoder的输入,Decoder有两个输入源。<br>
<img src="https://img2024.cnblogs.com/blog/2884766/202509/2884766-20250910151054098-1078969997.png" alt="输入源" loading="lazy"></p>
<pre><code>1. 目标序列的右移。
1. Encoder的输出。
</code></pre>
<p>​        <strong>Encoder的输出</strong>好理解,那么<strong>目标序列的右移</strong>是什么玩意儿?</p>
<h3 id="目标序列的右移">目标序列的右移</h3>
<p>​        我们继续发挥想象力,<strong>先不看Decoder的整体结构</strong>,而是直接先看这个流程<strong>(Encoder--&gt;Decoder--&gt;输出)</strong>。</p>
<p><strong>通俗理解:</strong></p>
<blockquote>
<p>​        Decoder拿到Encoder处理好的”我是人“的句子,在训练时,我们希望让他明白下一个字是”类“字(即”我是人类“这个句子)。</p>
<p>​        我们不是把完整的答案”我是人类“ 直接给解码器,而是将最后一个词给盖住,让他去猜。就是给他”我是人“这个句子,让他去猜下一个词,直到猜对来训练他。</p>
</blockquote>
<p><strong>原文理解:</strong></p>
<blockquote>
<p>​        Encoder处理完整输入序列 <code>[“我”, “是”, “人”]</code>,并输出一个包含所有信息的“上下文向量”。</p>
<p>​        在训练的时候,我们不是把完整的答案 <code>[“我”, “是”, “人”,“类”]</code> 直接给解码器。而是制作一个“右移”的版本:<code>[&lt;start&gt;, “我”, “是”,“人”]</code> 作为解码器的输入。</p>
<p>​        Decoder接收 <code>[&lt;start&gt;, “我”, “是”,“人”]</code> 和编码器的上下文向量。解码器基于这些信息,一步步地计算输出。</p>
<p>​        Decoder尝试预测<strong>下一个词</strong>,我们希望它的第一个输出是 <code>“我”</code>,第二个输出是 <code>“是”</code>,第三个输出是 <code>“人”</code>,第四个输出是<code>“类”</code>。</p>
<p>​        我们将解码器的预测结果 (<code>[“我”, “是”, “人”,“X”]</code>) 与真正的答案 (<code>[“我”, “是”, “人”,“类”]</code>) 进行比较,计算损失并更新模型权重。</p>
</blockquote>
<p>​        我们可以对比看到每次Decoder的输入和输出,就可以发现一个比较明显的右移现象。模型<strong>故意</strong>把解码器的输入(目标语句)<strong>整体向右移动了一位</strong>,目的是为了<strong>“欺骗”</strong>模型,让它学会根据“已经生成的词”来预测“下一个词”。</p>
<h3 id="掩码多头注意力机制">掩码多头注意力机制</h3>
<p><img src="https://img2024.cnblogs.com/blog/2884766/202509/2884766-20250910151125989-1188745634.png" alt="掩码多头" loading="lazy"></p>
<p>​        我们首先将上面的右移的目标语句作为输入记为<span class="math inline">\(X_1\)</span> ,他经过经典的<strong>位置编码</strong>后,传入到了一个特殊的多头注意力机制——掩码多头注意力机制中。</p>
<p>​        相较于普通的多头注意力机制,它多加了一层名为<strong>掩码</strong>的操作。</p>
<h4 id="掩码">掩码</h4>
<p>​        掩码,顾名思义,是要掩盖一些东西。正如它的名字一样,掩码的作用就是在训练时将一些词给“盖住”,不让注意力机制注意到,或者说模型给训练到。</p>
<p>​        它的原理解释起来也很简单,就是在预测或者翻译的过程中,不管是我们人来还是让模型来,都是要一句一句来<strong>顺序学习</strong>的。所以掩码的作用就是:<strong>掩盖住当前学习词后面的词语,防止模型过早地知道“答案”</strong>。</p>
<p><strong>通俗理解:</strong></p>
<blockquote>
<p>举个例子,模型在学习“我是人类”这个句子的时候,当前他的输入只是“我是人”。</p>
<p>为了防止模型过早学习到这个答案,就会用掩码将“类”掩盖起来,让模型先去猜(推测出下一个词)</p>
</blockquote>
<p>​        具体的结构上,我们在训练时,我们需要构造一个大小为k*k的下三角的单位矩阵(下三角为1,其余为-∞),k为输入词序列的长度。这样就可以实现将当前词后续的内容给掩盖掉的效果,让模型暂时不能学习到。</p>
<blockquote>
<p>所以这个掩码是放在<span class="math inline">\(Q * K^T\)</span>计算后,与V相乘之前参与运算的。</p>
</blockquote>
<h4 id="掩码类型">掩码类型</h4>
<p>​        为什么我在说明了掩码多头注意力机制后再去讲掩码类型呢,因为其实在注意力机制中,不只一种掩码存在,其实在之前我们已经使用了掩码。</p>
<p>​        在之前的普通多头注意力中,我们就使用了一种名叫<strong>padding mask</strong>的掩码技术。</p>
<p>​        在我们之前计算所有词的Q和K的相似度的时候进行了<span class="math inline">\(Q * K^T\)</span>的计算。在实际的训练中,我们不可能一条一条的语料让模型处理,当然是把所有语料都放给模型去训练,这个时候难免会出现<strong>句子长度不一</strong>的情况。那么这个时候,<strong>padding mask就起到了一个填充的作用</strong>。</p>
<p>​        它的用法很简单,打个比方就是给短的句子后面<strong>填充0</strong>让其长度变长。这个时候计算<span class="math inline">\(Q * K^T\)</span>的时候就可以进行运算了。当然在进行学习的时候,填充的内容当然不是我们想让模型学习到的东西,所以就相当于这部分是一个掩码信息不让模型进行学习。</p>
<h4 id="掩码多头注意力的输出">掩码多头注意力的输出</h4>
<p>​        这个时候,我们也是和Encoder的多头注意力模块一样,拼接各个“头”的掩码输出成为一个输出Z,当然这个输出和输入的向量维度相同。</p>
<h3 id="decoder的第二个多头注意力模块">Decoder的第二个多头注意力模块</h3>
<p>​        在经过了掩码多头注意力的输出与Add&amp;Norm之后,就到了第二个Decoder的多头注意力模块。这个多头注意力看上去跟Encoder的架构一样,但是它的输入却很奇怪。<br>
<img src="https://img2024.cnblogs.com/blog/2884766/202509/2884766-20250910151232727-1758796247.png" alt="第二个多头注意力" loading="lazy"></p>
<p>​        他的<strong>Q</strong>来自于同<strong>Decoder</strong>的掩码多头注意力机制中的输出<strong>,而他的</strong>K和V<strong>则是来自于</strong>Encoder层的输出**。</p>
<p>通俗理解:</p>
<blockquote>
<p>我们得到了当前的词语“我是人”和盖住的答案,要预测下一个词。它好奇根据语境的话下个词应该是什么(Q)</p>
<p>看看Encoder给出的原文,他开始查找一些关键信息</p>
<blockquote>
<p>Q = ← 来自Decoder的疑问<br>
K = ← Encoder提供的"关键词"<br>
V = ← Encoder提供的"详细解释"</p>
</blockquote>
<p>当处理最后一个字“人”的时候,它的内心OS(D_人)</p>
<blockquote>
<p>"在中文'我是人'这个语境中,'人'具体指什么?"</p>
</blockquote>
<p>Encoder通过注意力权重回答:</p>
<blockquote>
<p>"主要看'人'本身(70%),其次看'是'(20%),'我'影响较小(10%)"</p>
</blockquote>
<p>这样,模型可能就理解了包含了"人"在判断句中的特殊含义,识别出这不是单独的"人",而是"人类"概念。</p>
</blockquote>
<p>就是这样一个过程,让Decoder可以理解Encoder指示的内容去进行判断。也是这个第二个多头注意力机制所关注的内容。</p>
<h3 id="decoder的流程与训练">Decoder的流程与训练</h3>
<p>​        经过了我们的<strong>掩码多头注意力机制——&gt;Add&amp;Norm——&gt;多头注意力机制——&gt;Add&amp;Norm——&gt;Feed Forward——&gt;Add&amp;Norm</strong> 这样的一个结构,就构成了Decoder的结构。<br>
<img src="https://img2024.cnblogs.com/blog/2884766/202509/2884766-20250910151313327-1292788645.png" alt="Decoder" loading="lazy"></p>
<p>​        和Encoder一样,上面的Decoder过程也是反复进行了6次,Decoder已经对当前的这个词语已经完全理解了,现在的用处就是让他学会“说话”——也就是输出自己的答案。</p>
<h3 id="输出">输出</h3>
<p>​<img src="https://img2024.cnblogs.com/blog/2884766/202509/2884766-20250910151328404-248249309.png" alt="image" loading="lazy"></p>
<p>在这个部分,我们首先对上面Decoder的输出进行一个表示:<span class="math inline">\(Output Z ∈ ℝ^(batch_size × seq_len × d_model)\)</span> ,每个位置的向量已包含完整上下文信息,而最后一个位置的向量最"富含"预测下一个词的信息。</p>
<p>​        首先,我们需要将输出结果Z的最后一个位置进行预测。经过一次线性变换(全连接神经网络)和概率分布变换。通过这一个线性层,我们将这个结果的输出向量投影到一个词汇空间。</p>
<p>​        在这个维度上,模型对每个可能的词计算一个"匹配分数"(logit),分数越高,表示该词越可能出现在当前位置。然后,将这个结果通过Softmax将对应的分数转化为概率。</p>
<blockquote>
<p>想象一下,Decoder将自己的输出去查字典去了。然后比对词汇表的每一个词,看哪个词是概率最高的下一个词。这个字典是一开始训练的时候,模型根据学习的预料数据自己学习统计的。</p>
</blockquote>
<p>​        这样,我们的模型得到了下一个概率最高的字进行输出。这样下来,就算模型得到了自己的学习结果。如果实在训练过程中,我们需要对这个过程进行批改和纠正,就是让他和原始的训练数据“我是人类”进行loss计算,从而反复进行训练,直到达到最好的loss值。</p>
<p>​        这样就是一个完整的Decoder训练过程。</p>
<h2 id="decoder结构推理">Decoder结构(推理)</h2>
<p><img src="https://img2024.cnblogs.com/blog/2884766/202509/2884766-20250910151441613-1848175891.png" alt="循环推理" loading="lazy"></p>
<p>​        当我们已经训练好一个Transformer架构了,我们现在想要使用它进行预测。我们的输入还是“我是人”。这样,Encoder已经完成了语义的分析与注意力关注,接下来的重点就是Decoder的推理了。那么它可能是:</p>
<p><strong>通俗理解:</strong></p>
<blockquote>
<p>​        我们的Decoder相当于一个预言家,它开始拿到我们Encoder给他处理好的句子”我是人“。此时Decoder已经学会了预测的方法(参透了符文的力量哈哈)。</p>
<p>​        它开始一个词一个词的预测,先是从”类“开始。然后它再猜下一个词是什么,它隐隐约约觉得下一个词好像是”你“……</p>
<p>​        最后,Decoder下定决心,它感觉有一股冥冥之中的旨意告诉他,让他根据自己的训练数据生成一段完整的话:</p>
<p>​        “我是人类你是人吗?”</p>
</blockquote>
<p><strong>原文理解:</strong></p>
<blockquote>
<p>​        在原文中,训练好的Decoder从起止符开始进行预测,它已经通过自回归学习让它从盲猜中找到规律。</p>
<p>​        <strong>开始</strong>:给定一个起始符<code>&lt;start&gt;</code>、“我”、“是” 、”人“<strong>(输入为”<code>&lt;s&gt;</code>,我,是,人“)</strong>,预测第一个词 “类”。<strong>(输出为”我是人类“)</strong></p>
<p>​        <strong>迭代</strong>:将<code>&lt;start&gt;</code>、“我”、“是” 、”人“ 和 “类” 一起输入<strong>(输入为”<code>&lt;s&gt;</code>,我,是,人,类“)</strong>,预测下一个词 “你”。<strong>(输出为”我是人类你“)</strong></p>
<p>​        ……</p>
<p>​        <strong>结束</strong>:直到预测出结束符<code>&lt;end&gt;</code>,生成结束。<strong>(输出为“我是人类你是人吗?”)</strong></p>
</blockquote><br><br>
来源:https://www.cnblogs.com/qichengxiaoqi/p/19082685
頁: [1]
查看完整版本: Transformer通俗讲解(大白话版)