<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Transformer on On My Way</title>
    <link>https://tuanzi-wow.cn/tags/transformer/</link>
    <description>Recent content in Transformer on On My Way</description>
    <generator>Hugo</generator>
    <language>zh-CN</language>
    <lastBuildDate>Sun, 06 Sep 2026 00:00:00 +0800</lastBuildDate>
    <atom:link href="https://tuanzi-wow.cn/tags/transformer/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Transformer 架构</title>
      <link>https://tuanzi-wow.cn/llm/llm-transformer/</link>
      <pubDate>Sun, 06 Sep 2026 00:00:00 +0800</pubDate>
      <guid>https://tuanzi-wow.cn/llm/llm-transformer/</guid>
      <description>Transformer 笔记：注意力机制、Encoder-Decoder 结构与 PyTorch 逐块实现。</description>
      <content:encoded><![CDATA[<h2 id="transformer框架">Transformer框架</h2>
<h2 id="21-注意力机制">2.1 注意力机制</h2>
<h3 id="211-什么是注意力机制">2.1.1 什么是注意力机制</h3>
<p>从计算机视觉为起源发展起来的神经网络,其核心架构有三种</p>
<ul>
<li><strong>前馈神经网络（Feedforward Neural Network，FNN）</strong>，即数据从输入层单向流动到输出层，无循环结构，各层之间通过全连接或特定方式传递信息，其中多层感知机（Multi-Layer Perceptron，MLP）是最常见的形式，每一层的神经元都和上下两层的每一个神经元完全连接，如图2.1所示:</li>
</ul>
<p><img alt="图片描述" loading="lazy" src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/2-figures/1-0.png"></p>
<p>图2.1 全连接神经网络</p>
<ul>
<li><strong>卷积神经网络（Convolutional Neural Network，CNN）</strong>，即训练参数量远小于全连接神经网络的卷积层来进行特征提取和学习，如图2.2所示:</li>
</ul>
<p><img alt="图片描述" loading="lazy" src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/2-figures/1-1.png"></p>
<p>图2.2 卷积神经网络</p>
<ul>
<li><strong>循环神经网络（Recurrent Neural Network，RNN）</strong>，能够使用历史信息作为输入、包含环和自重复的网络，如图2.3所示:</li>
</ul>
<p><img alt="图片描述" loading="lazy" src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/2-figures/1-2.png"></p>
<p>由于 NLP 任务所需要处理的文本往往是序列，因此专用于处理序列、时序数据的 RNN 往往能够在 NLP 任务上取得最优的效果</p>
<p>虽然RNN及LSTM具有捕捉时序信息,适合序列生成的优点,却有两个难以弥补的缺陷:
1.序列依序计算的模式能够很好地模拟时序信息,但是限制了<strong>计算机并行计算的能力</strong>
2.RNN难以捕捉<strong>长序列的相关关系</strong></p>
<p><strong>什么是注意力机制呢?</strong></p>
<p>核心思想:将重点注意力集中在一个或者几个Token,从而取得更高效高质的计算效果
三个核心变量:Query(查寻值),Key(键值),Value(真值):</p>
<p>一个例子方便理解,可看可不看:</p>
<p>当我们有一篇新闻报道，我们想要找到这个报道的时间，那么，我们的 Query 可以是类似于“时间”、“日期”一类的向量（为了便于理解，此处使用文本来表示，但其实际是稠密的向量），Key 和 Value 会是整个文本。通过对 Query 和 Key 进行运算我们可以得到一个权重，这个权重其实反映了从 Query 出发，对文本每一个 token 应该分布的注意力相对大小。通过把权重和 Value 进行运算，得到的最后结果就是从 Query 出发计算整个文本注意力得到的结果。</p>
<p>注意力机制的特点是通过计算 <strong>Query</strong> 与<strong>Key</strong>的相关性为真值加权求和，从而拟合序列中每个词同其他词的相关关系。</p>
<h3 id="212-深入理解注意力机制">2.1.2 深入理解注意力机制</h3>
<p>给不同 Key 所赋予的不同权重，就是我们所说的注意力分数，也就是为了查询到 Query，我们应该赋予给每一个 Key 多少注意力
但是，如何针对每一个 Query，计算出对应的注意力分数呢？从直观上讲，我们可以认为 Key 与 Query 相关性越高，则其所应该赋予的注意力权重就越大。但是，我们如何能够找到一个合理的、能够计算出正确的注意力分数的方法呢
$$
v \cdot w = \sum_i v_i w_i
$$
根据词向量的定义，语义相似的两个词对应的词向量的点积应该大于 0，而语义不相似的词向量点积应该小于 0。</p>
<p>那么，我们就可以用点积来计算词之间的相似度。假设我们的 Query 为“fruit”，对应的词向量为 <code>q</code>；我们的 Key 对应的词向量为：
$$
K=\begin{bmatrix} v_{\text{apple}} \ v_{\text{banana}} \ v_{\text{chair}} \end{bmatrix}
$$
则我们可以计算 Query 和每一个 Key 的相似程度：
$$
x = qK^T
$$
此处的 <code>K</code> 即为将所有 Key 对应的词向量堆叠形成的矩阵。基于矩阵乘法的定义，<code>x</code> 即为 <code>q</code> 与每一个 Key 向量的点积。</p>
<p>现在我们得到的 <code>x</code> 即反映了 Query 和每一个 Key 的相似程度，我们再通过一个 Softmax 层将其转化为和为 1 的权重：
$$
\operatorname{softmax}(x)_i = \frac{e^{x_i}}{\sum_j e^{x_j}}
$$
这样，得到的向量就能够反映 Query 和每一个 Key 的相似程度，同时所有权重相加为 1，也就是我们的注意力分数。</p>
<p>最后，我们再将得到的注意力分数和值向量做对应乘积即可。根据上述过程，我们就可以得到注意力机制计算的基本公式：
$$
\operatorname{Attention}(Q,K,V) = \operatorname{softmax}(qK^T)V
$$
不过，此时的值还是一个标量，同时，我们此次只查询了一个 Query。我们可以将值转化为维度为 <code>d_v</code> 的向量，同时一次性查询多个 Query，同样将多个 Query 对应的词向量堆叠在一起形成矩阵 <code>Q</code>，得到公式：
$$
\operatorname{Attention}(Q,K,V) = \operatorname{softmax}(QK^T)V
$$
目前，我们离标准的注意力机制公式还差最后一步。在上一个公式中，如果 Q 和 K 对应的维度 <code>d_k</code> 比较大，<code>QK^T</code> 的结果数值可能会变得较大，使 Softmax 输出过于集中，从而影响梯度的稳定性。因此，我们要将 Q 和 K 乘积的结果做一个缩放：
$$
\operatorname{Attention}(Q,K,V) = \operatorname{softmax} \left( \frac{QK^T}{\sqrt{d_k}} \right)V
$$</p>
<h3 id="213-注意力机制的实现">2.1.3 注意力机制的实现</h3>
<p>使用Pytorch</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">&#39;&#39;&#39;注意力计算函数&#39;&#39;&#39;
</span></span><span class="line"><span class="cl">def attention(query, key, value, dropout=None):
</span></span><span class="line"><span class="cl">    &#39;&#39;&#39;
</span></span><span class="line"><span class="cl">    args:
</span></span><span class="line"><span class="cl">    query: 查询值矩阵
</span></span><span class="line"><span class="cl">    key: 键值矩阵
</span></span><span class="line"><span class="cl">    value: 真值矩阵
</span></span><span class="line"><span class="cl">    &#39;&#39;&#39;
</span></span><span class="line"><span class="cl">    # 获取键向量的维度，键向量的维度和值向量的维度相同
</span></span><span class="line"><span class="cl">    d_k = query.size(-1) 
</span></span><span class="line"><span class="cl">    # 计算Q与K的内积并除以根号dk
</span></span><span class="line"><span class="cl">    # transpose——相当于转置
</span></span><span class="line"><span class="cl">    scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k)
</span></span><span class="line"><span class="cl">    # Softmax
</span></span><span class="line"><span class="cl">    p_attn = scores.softmax(dim=-1)
</span></span><span class="line"><span class="cl">    if dropout is not None:
</span></span><span class="line"><span class="cl">        p_attn = dropout(p_attn)
</span></span><span class="line"><span class="cl">        # 采样
</span></span><span class="line"><span class="cl">     # 根据计算结果对value进行加权求和
</span></span><span class="line"><span class="cl">    return torch.matmul(p_attn, value), p_attn
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="214-自注意力">2.1.4 自注意力</h3>
<p>注意力机制的本质是对两段序列的元素依次进行相似度计算，寻找出一个序列的每个元素对另一个序列的每个元素的相关度，然后基于相关度进行加权，即分配注意力。而这两段序列即是我们计算过程中 Q、K、V 的来源</p>
<p>但是，在我们的实际应用中，我们往往只需要计算 Query 和 Key 之间的注意力结果，很少存在额外的真值 Value。也就是说，我们<strong>其实只需要拟合两个文本序列。在经典的 注意力机制中，Q 往往来自于一个序列，K 与 V 来自于另一个序列</strong>，都通过参数矩阵计算得到，从而可以拟合这两个序列之间的关系。例如在 Transformer 的 Decoder 结构中，Q 来自于 Decoder 的输入，K 与 V 来自于 Encoder 的输出，从而拟合了编码信息与历史信息之间的关系，便于综合这两种信息实现未来的预测。</p>
<p>但在 Transformer 的 Encoder 结构中，使用的是 注意力机制的变种 —— 自注意力（self-attention，自注意力）机制。所谓自注意力，即是<strong>计算本身序列中每个元素对其他元素的注意力分布</strong>，即在计算过程中，Q、K、V 都由<strong>同一个输入通过不同的参数矩阵</strong>计算得到。在 Encoder 中，Q、K、V 分别是输入对参数矩阵 Wq、Wk、Wv<em>W**q</em>、<em>W**k</em>、<em>W**v</em> 做积得到，从而拟合输入语句中每一个 token 对其他所有 token 的关系。</p>
<p>通过自注意力机制，我们可以找到一段文本中每一个 token 与其他所有 token 的相关关系大小，从而建模文本之间的依赖关系。在代码中的实现，self-attention 机制其实是通过给 Q、K、V 的输入传入同一个参数实现的：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># attention 为上文定义的注意力计算函数</span>
</span></span><span class="line"><span class="cl"><span class="n">attention</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">x</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="215-掩码自注意力">2.1.5 掩码自注意力</h3>
<p>掩码自注意力，即 Mask Self-Attention，是指使用注意力掩码的自注意力机制。掩码的作用是遮蔽一些特定位置的 token，模型在学习的过程中，会忽略掉被遮蔽的 token。
使用注意力掩码的核心动机是让模型只能使用历史信息进行预测而不能看到未来信息</p>
<p>使用注意力机制的 Transformer 模型也是通过类似于 n-gram 的语言模型任务来学习的
例子:如，如果待学习的文本序列是 【BOS】I like you【EOS】，那么，模型会按如下顺序进行预测和学习：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">Step 1：输入 【BOS】，输出 I
</span></span><span class="line"><span class="cl">Step 2：输入 【BOS】I，输出 like
</span></span><span class="line"><span class="cl">Step 3：输入 【BOS】I like，输出 you
</span></span><span class="line"><span class="cl">Step 4：输入 【BOS】I like you，输出 【EOS】
</span></span></code></pre></td></tr></table>
</div>
</div><p>但是上述过程是一个串行的过程,计算效率很低
针对这个问题，Transformer 就提出了掩码自注意力的方法掩码自注意力会生成一串掩码，来遮蔽未来信息。例如，我们待学习的文本序列仍然是 【BOS】I like you【EOS】，我们使用的注意力掩码是【MASK】，那么模型的输入为：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">&lt;BOS&gt; 【MASK】【MASK】【MASK】【MASK】
</span></span><span class="line"><span class="cl">&lt;BOS&gt;    I   【MASK】 【MASK】【MASK】
</span></span><span class="line"><span class="cl">&lt;BOS&gt;    I     like  【MASK】【MASK】
</span></span><span class="line"><span class="cl">&lt;BOS&gt;    I     like    you  【MASK】
</span></span><span class="line"><span class="cl">&lt;BOS&gt;    I     like    you   &lt;/EOS&gt;
</span></span></code></pre></td></tr></table>
</div>
</div><p>上述输入不再是串行的过程，而可以一起并行地输入到模型中</p>
<p>观察掩码发现:是一个和文本序列等长的上三角矩阵
我们可以简单地通过创建一个和输入同等长度的上三角矩阵作为注意力掩码，再使用掩码来遮蔽掉输入即可为注意力掩码，再使用掩码来遮蔽掉输入即可。也就是说，当输入维度为 （batch_size, seq_len, hidden_size）时，我们的 Mask 矩阵维度一般为 (1, seq_len, seq_len)（通过广播实现同一个 batch 中不同样本的计算）。</p>
<p>在具体实现中，我们通过以下代码生成 Mask 矩阵：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># 创建一个上三角矩阵，用于遮蔽未来信息。</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 先通过 full 函数创建一个 1 * seq_len * seq_len 的矩阵</span>
</span></span><span class="line"><span class="cl"><span class="n">mask</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">full</span><span class="p">((</span><span class="mi">1</span><span class="p">,</span> <span class="n">args</span><span class="o">.</span><span class="n">max_seq_len</span><span class="p">,</span> <span class="n">args</span><span class="o">.</span><span class="n">max_seq_len</span><span class="p">),</span> <span class="nb">float</span><span class="p">(</span><span class="s2">&#34;-inf&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"><span class="c1"># triu 函数的功能是创建一个上三角矩阵</span>
</span></span><span class="line"><span class="cl"><span class="n">mask</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">triu</span><span class="p">(</span><span class="n">mask</span><span class="p">,</span> <span class="n">diagonal</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>生成的 Mask 矩阵会是一个上三角矩阵，上三角位置的元素均为 -inf，其他位置的元素置为0。</p>
<p>在注意力计算时，我们会将计算得到的注意力分数与这个掩码做和，再进行 Softmax 操作：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># 此处的 scores 为计算得到的注意力分数，mask 为上文生成的掩码矩阵</span>
</span></span><span class="line"><span class="cl"><span class="n">scores</span> <span class="o">=</span> <span class="n">scores</span> <span class="o">+</span> <span class="n">mask</span><span class="p">[:,</span> <span class="p">:</span><span class="n">seqlen</span><span class="p">,</span> <span class="p">:</span><span class="n">seqlen</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="n">scores</span> <span class="o">=</span> <span class="n">F</span><span class="o">.</span><span class="n">softmax</span><span class="p">(</span><span class="n">scores</span><span class="o">.</span><span class="n">float</span><span class="p">(),</span> <span class="n">dim</span><span class="o">=-</span><span class="mi">1</span><span class="p">)</span><span class="o">.</span><span class="n">type_as</span><span class="p">(</span><span class="n">xq</span><span class="p">)</span><span class="n">Copy</span> <span class="n">to</span> <span class="n">clipboardErrorCopied</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>通过做求和，上三角区域（也就是应该被遮蔽的 token 对应的位置）的注意力分数结果都变成了 <code>-inf</code>，而下三角区域的分数不变。再做 Softmax 操作，<code>-inf</code> 的值在经过 Softmax 之后会被置为 0，从而忽略了上三角区域计算的注意力分数，从而实现了注意力遮蔽。</p>
<h3 id="216-多头注意力">2.1.6 多头注意力</h3>
<p>注意力机制可以实现并行化与长期依赖关系拟合，但一次注意力计算只能拟合一种相关关系，单一的注意力机制很难全面拟合语句序列里的相关关系</p>
<p>因此 Transformer 使用了多头注意力机制（Multi-Head Attention），即同时对一个语料进行多次注意力计算，每次注意力计算都能拟合不同的关系，将最后的多次结果拼接起来作为最后的输出，即可更全面深入地拟合语言信息。</p>
<p>事实上，所谓的多头注意力机制其实就是将原始的输入序列进行多组的自注意力处理；然后再将每一组得到的自注意力结果拼接起来，再通过一个线性层进行处理，得到最终的输出。我们用公式可以表示为：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">\operatorname{MultiHead}(Q,K,V)
</span></span><span class="line"><span class="cl">=
</span></span><span class="line"><span class="cl">\operatorname{Concat}(\operatorname{head}_1,\ldots,\operatorname{head}_h)W^O
</span></span></code></pre></td></tr></table>
</div>
</div><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span><span class="lnt">32
</span><span class="lnt">33
</span><span class="lnt">34
</span><span class="lnt">35
</span><span class="lnt">36
</span><span class="lnt">37
</span><span class="lnt">38
</span><span class="lnt">39
</span><span class="lnt">40
</span><span class="lnt">41
</span><span class="lnt">42
</span><span class="lnt">43
</span><span class="lnt">44
</span><span class="lnt">45
</span><span class="lnt">46
</span><span class="lnt">47
</span><span class="lnt">48
</span><span class="lnt">49
</span><span class="lnt">50
</span><span class="lnt">51
</span><span class="lnt">52
</span><span class="lnt">53
</span><span class="lnt">54
</span><span class="lnt">55
</span><span class="lnt">56
</span><span class="lnt">57
</span><span class="lnt">58
</span><span class="lnt">59
</span><span class="lnt">60
</span><span class="lnt">61
</span><span class="lnt">62
</span><span class="lnt">63
</span><span class="lnt">64
</span><span class="lnt">65
</span><span class="lnt">66
</span><span class="lnt">67
</span><span class="lnt">68
</span><span class="lnt">69
</span><span class="lnt">70
</span><span class="lnt">71
</span><span class="lnt">72
</span><span class="lnt">73
</span><span class="lnt">74
</span><span class="lnt">75
</span><span class="lnt">76
</span><span class="lnt">77
</span><span class="lnt">78
</span><span class="lnt">79
</span><span class="lnt">80
</span><span class="lnt">81
</span><span class="lnt">82
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">torch.nn</span> <span class="k">as</span> <span class="nn">nn</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">torch</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="s1">&#39;&#39;&#39;多头自注意力计算模块&#39;&#39;&#39;</span>
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">MultiHeadAttention</span><span class="p">(</span><span class="n">nn</span><span class="o">.</span><span class="n">Module</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">args</span><span class="p">:</span> <span class="n">ModelArgs</span><span class="p">,</span> <span class="n">is_causal</span><span class="o">=</span><span class="kc">False</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># 构造函数</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># args: 配置对象</span>
</span></span><span class="line"><span class="cl">        <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="fm">__init__</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># 隐藏层维度必须是头数的整数倍，因为后面我们会将输入拆成头数个矩阵</span>
</span></span><span class="line"><span class="cl">        <span class="k">assert</span> <span class="n">args</span><span class="o">.</span><span class="n">dim</span> <span class="o">%</span> <span class="n">args</span><span class="o">.</span><span class="n">n_heads</span> <span class="o">==</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># 每个头的维度，等于模型维度除以头的总数。</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">head_dim</span> <span class="o">=</span> <span class="n">args</span><span class="o">.</span><span class="n">dim</span> <span class="o">//</span> <span class="n">args</span><span class="o">.</span><span class="n">n_heads</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">n_heads</span> <span class="o">=</span> <span class="n">args</span><span class="o">.</span><span class="n">n_heads</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1"># Wq, Wk, Wv 参数矩阵，每个参数矩阵为 n_embd x dim</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># 这里通过三个组合矩阵来代替了n个参数矩阵的组合，其逻辑在于矩阵内积再拼接其实等同于拼接矩阵再内积，</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># 不理解的读者可以自行模拟一下，每一个线性层其实相当于n个参数矩阵的拼接</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">wq</span> <span class="o">=</span> <span class="n">nn</span><span class="o">.</span><span class="n">Linear</span><span class="p">(</span><span class="n">args</span><span class="o">.</span><span class="n">n_embd</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">n_heads</span> <span class="o">*</span> <span class="bp">self</span><span class="o">.</span><span class="n">head_dim</span><span class="p">,</span> <span class="n">bias</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">wk</span> <span class="o">=</span> <span class="n">nn</span><span class="o">.</span><span class="n">Linear</span><span class="p">(</span><span class="n">args</span><span class="o">.</span><span class="n">n_embd</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">n_heads</span> <span class="o">*</span> <span class="bp">self</span><span class="o">.</span><span class="n">head_dim</span><span class="p">,</span> <span class="n">bias</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">wv</span> <span class="o">=</span> <span class="n">nn</span><span class="o">.</span><span class="n">Linear</span><span class="p">(</span><span class="n">args</span><span class="o">.</span><span class="n">n_embd</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">n_heads</span> <span class="o">*</span> <span class="bp">self</span><span class="o">.</span><span class="n">head_dim</span><span class="p">,</span> <span class="n">bias</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># 输出权重矩阵，维度为 dim x dim（head_dim = dim / n_heads）</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">wo</span> <span class="o">=</span> <span class="n">nn</span><span class="o">.</span><span class="n">Linear</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">n_heads</span> <span class="o">*</span> <span class="bp">self</span><span class="o">.</span><span class="n">head_dim</span><span class="p">,</span> <span class="n">args</span><span class="o">.</span><span class="n">dim</span><span class="p">,</span> <span class="n">bias</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># 注意力的 dropout</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">attn_dropout</span> <span class="o">=</span> <span class="n">nn</span><span class="o">.</span><span class="n">Dropout</span><span class="p">(</span><span class="n">args</span><span class="o">.</span><span class="n">dropout</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># 残差连接的 dropout</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">resid_dropout</span> <span class="o">=</span> <span class="n">nn</span><span class="o">.</span><span class="n">Dropout</span><span class="p">(</span><span class="n">args</span><span class="o">.</span><span class="n">dropout</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">is_causal</span> <span class="o">=</span> <span class="n">is_causal</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1"># 创建一个上三角矩阵，用于遮蔽未来信息</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># 注意，因为是多头注意力，Mask 矩阵比之前我们定义的多一个维度</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">is_causal</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">mask</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">full</span><span class="p">((</span><span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="n">args</span><span class="o">.</span><span class="n">max_seq_len</span><span class="p">,</span> <span class="n">args</span><span class="o">.</span><span class="n">max_seq_len</span><span class="p">),</span> <span class="nb">float</span><span class="p">(</span><span class="s2">&#34;-inf&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">            <span class="n">mask</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">triu</span><span class="p">(</span><span class="n">mask</span><span class="p">,</span> <span class="n">diagonal</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="c1"># 注册为模型的缓冲区</span>
</span></span><span class="line"><span class="cl">            <span class="bp">self</span><span class="o">.</span><span class="n">register_buffer</span><span class="p">(</span><span class="s2">&#34;mask&#34;</span><span class="p">,</span> <span class="n">mask</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">forward</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">q</span><span class="p">:</span> <span class="n">torch</span><span class="o">.</span><span class="n">Tensor</span><span class="p">,</span> <span class="n">k</span><span class="p">:</span> <span class="n">torch</span><span class="o">.</span><span class="n">Tensor</span><span class="p">,</span> <span class="n">v</span><span class="p">:</span> <span class="n">torch</span><span class="o">.</span><span class="n">Tensor</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1"># 获取批次大小和序列长度，[batch_size, seq_len, dim]</span>
</span></span><span class="line"><span class="cl">        <span class="n">bsz</span><span class="p">,</span> <span class="n">seqlen</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">q</span><span class="o">.</span><span class="n">shape</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1"># 计算查询（Q）、键（K）、值（V）,输入通过参数矩阵层，维度为 (B, T, n_embed) x (n_embed, dim) -&gt; (B, T, dim)</span>
</span></span><span class="line"><span class="cl">        <span class="n">xq</span><span class="p">,</span> <span class="n">xk</span><span class="p">,</span> <span class="n">xv</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">wq</span><span class="p">(</span><span class="n">q</span><span class="p">),</span> <span class="bp">self</span><span class="o">.</span><span class="n">wk</span><span class="p">(</span><span class="n">k</span><span class="p">),</span> <span class="bp">self</span><span class="o">.</span><span class="n">wv</span><span class="p">(</span><span class="n">v</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1"># 将 Q、K、V 拆分成多头，维度为 (B, T, n_head, dim // n_head)，然后交换维度，变成 (B, n_head, T, dim // n_head)</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># 因为在注意力计算中我们是取了后两个维度参与计算</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># 为什么要先按B*T*n_head*C//n_head展开再互换1、2维度而不是直接按注意力输入展开，是因为view的展开方式是直接把输入全部排开，</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># 然后按要求构造，可以发现只有上述操作能够实现我们将每个头对应部分取出来的目标</span>
</span></span><span class="line"><span class="cl">        <span class="n">xq</span> <span class="o">=</span> <span class="n">xq</span><span class="o">.</span><span class="n">view</span><span class="p">(</span><span class="n">bsz</span><span class="p">,</span> <span class="n">seqlen</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">n_heads</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">head_dim</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">xk</span> <span class="o">=</span> <span class="n">xk</span><span class="o">.</span><span class="n">view</span><span class="p">(</span><span class="n">bsz</span><span class="p">,</span> <span class="n">seqlen</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">n_heads</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">head_dim</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">xv</span> <span class="o">=</span> <span class="n">xv</span><span class="o">.</span><span class="n">view</span><span class="p">(</span><span class="n">bsz</span><span class="p">,</span> <span class="n">seqlen</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">n_heads</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">head_dim</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">xq</span> <span class="o">=</span> <span class="n">xq</span><span class="o">.</span><span class="n">transpose</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">xk</span> <span class="o">=</span> <span class="n">xk</span><span class="o">.</span><span class="n">transpose</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">xv</span> <span class="o">=</span> <span class="n">xv</span><span class="o">.</span><span class="n">transpose</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1"># 注意力计算</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># 计算 QK^T / sqrt(d_k)，维度为 (B, nh, T, hs) x (B, nh, hs, T) -&gt; (B, nh, T, T)</span>
</span></span><span class="line"><span class="cl">        <span class="n">scores</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">matmul</span><span class="p">(</span><span class="n">xq</span><span class="p">,</span> <span class="n">xk</span><span class="o">.</span><span class="n">transpose</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">))</span> <span class="o">/</span> <span class="n">math</span><span class="o">.</span><span class="n">sqrt</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">head_dim</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># 掩码自注意力必须有注意力掩码</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">is_causal</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">assert</span> <span class="nb">hasattr</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="s1">&#39;mask&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="c1"># 这里截取到序列长度，因为有些序列可能比 max_seq_len 短</span>
</span></span><span class="line"><span class="cl">            <span class="n">scores</span> <span class="o">=</span> <span class="n">scores</span> <span class="o">+</span> <span class="bp">self</span><span class="o">.</span><span class="n">mask</span><span class="p">[:,</span> <span class="p">:,</span> <span class="p">:</span><span class="n">seqlen</span><span class="p">,</span> <span class="p">:</span><span class="n">seqlen</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># 计算 softmax，维度为 (B, nh, T, T)</span>
</span></span><span class="line"><span class="cl">        <span class="n">scores</span> <span class="o">=</span> <span class="n">F</span><span class="o">.</span><span class="n">softmax</span><span class="p">(</span><span class="n">scores</span><span class="o">.</span><span class="n">float</span><span class="p">(),</span> <span class="n">dim</span><span class="o">=-</span><span class="mi">1</span><span class="p">)</span><span class="o">.</span><span class="n">type_as</span><span class="p">(</span><span class="n">xq</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># 做 Dropout</span>
</span></span><span class="line"><span class="cl">        <span class="n">scores</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">attn_dropout</span><span class="p">(</span><span class="n">scores</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># V * Score，维度为(B, nh, T, T) x (B, nh, T, hs) -&gt; (B, nh, T, hs)</span>
</span></span><span class="line"><span class="cl">        <span class="n">output</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">matmul</span><span class="p">(</span><span class="n">scores</span><span class="p">,</span> <span class="n">xv</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1"># 恢复时间维度并合并头。</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># 将多头的结果拼接起来, 先交换维度为 (B, T, n_head, dim // n_head)，再拼接成 (B, T, n_head * dim // n_head)</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># contiguous 函数用于重新开辟一块新内存存储，因为Pytorch设置先transpose再view会报错，</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># 因为view直接基于底层存储得到，然而transpose并不会改变底层存储，因此需要额外存储</span>
</span></span><span class="line"><span class="cl">        <span class="n">output</span> <span class="o">=</span> <span class="n">output</span><span class="o">.</span><span class="n">transpose</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span><span class="o">.</span><span class="n">contiguous</span><span class="p">()</span><span class="o">.</span><span class="n">view</span><span class="p">(</span><span class="n">bsz</span><span class="p">,</span> <span class="n">seqlen</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1"># 最终投影回残差流。</span>
</span></span><span class="line"><span class="cl">        <span class="n">output</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">wo</span><span class="p">(</span><span class="n">output</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">output</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">resid_dropout</span><span class="p">(</span><span class="n">output</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">output</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="22-encoder-decoder">2.2 Encoder-Decoder</h2>
<p>Transformer框架的两个核心组件:
Encoder(编码器)和Decoder(解码器)</p>
<h3 id="221-seq2seq模型">2.2.1 Seq2Seq模型</h3>
<p>Seq2Seq:序列到序列
具体而言,
是指模型输入的是一个自然语言序列 input=(x1,x2,x3&hellip;xn)<em>in<strong>p</strong>u**t</em>=(<em>x</em>1,<em>x</em>2,<em>x</em>3&hellip;<em>x**n</em>) ，输出的是一个可能不等长的自然语言序列 output=(y1,y2,y3&hellip;ym)<em>o<strong>u</strong>tp<strong>u</strong>t</em>=(<em>y</em>1,<em>y</em>2,<em>y</em>3&hellip;<em>y**m</em>)</p>
<p>流程解析:
对于 Seq2Seq 任务，一般的思路是对自然语言序列进行编码再解码。所谓编码，就是将输入的自然语言序列通过隐藏层编码成能够表征语义的向量（或矩阵），可以简单理解为更复杂的词向量表示。而解码，就是对输入的自然语言序列编码得到的向量或矩阵通过隐藏层输出，再解码成对应的自然语言目标序列。通过编码再解码，就可以实现 Seq2Seq 任务。
而其中的编码和解码工作,就是有Encoder和Decoder完成的</p>
<p>Transformer的架构:</p>
<img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/2-figures/2-0.jpg" alt="图片描述" style="zoom:33%;" />
Transformer 由 Encoder 和 Decoder 组成，每一个 Encoder（Decoder）又由 6个 Encoder（Decoder)Layer 组成。输入源序列会进入 Encoder 进行编码，到 Encoder Layer 的最顶层再将编码结果输出给 Decoder Layer 的每一层，通过 Decoder 解码后就可以得到输出目标序列了
<p>Encoder 和 Decoder 内部传统神经网络的经典结构——前馈神经网络（FNN）、层归一化（Layer Norm）和残差连接（Residual Connection）</p>
<h3 id="222-前馈神经网络fnn">2.2.2 前馈神经网络(FNN)</h3>
<p>每一层的神经元都和上下两层的每一个神经元完全连接的网络结构
每一个 Encoder Layer 都包含一个上文讲的注意力机制和一个前馈神经网络</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">class MLP(nn.Module):
</span></span><span class="line"><span class="cl">    &#39;&#39;&#39;前馈神经网络&#39;&#39;&#39;
</span></span><span class="line"><span class="cl">    def __init__(self, dim: int, hidden_dim: int, dropout: float):
</span></span><span class="line"><span class="cl">        super().__init__()
</span></span><span class="line"><span class="cl">        # 定义第一层线性变换，从输入维度到隐藏维度
</span></span><span class="line"><span class="cl">        self.w1 = nn.Linear(dim, hidden_dim, bias=False)
</span></span><span class="line"><span class="cl">        # 定义第二层线性变换，从隐藏维度到输入维度
</span></span><span class="line"><span class="cl">        self.w2 = nn.Linear(hidden_dim, dim, bias=False)
</span></span><span class="line"><span class="cl">        # 定义dropout层，用于防止过拟合
</span></span><span class="line"><span class="cl">        self.dropout = nn.Dropout(dropout)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    def forward(self, x):
</span></span><span class="line"><span class="cl">        # 前向传播函数
</span></span><span class="line"><span class="cl">        # 首先，输入x通过第一层线性变换和RELU激活函数
</span></span><span class="line"><span class="cl">        # 最后，通过第二层线性变换和dropout层
</span></span><span class="line"><span class="cl">        return self.dropout(self.w2(F.relu(self.w1(x))))
</span></span><span class="line"><span class="cl">    
</span></span></code></pre></td></tr></table>
</div>
</div><p>Transformer的FNN的架构:
由两个线性层中间加一个 RELU 激活函数组成的，以及前馈神经网络还加入了一个 Dropout 层来防止过拟合
Dropout 层只在训练时开启，推理/测试阶段关闭</p>
<h3 id="223-层归一化layer-norm">2.2.3 层归一化(Layer Norm)</h3>
<p>神经网络的主流归一化操作有:
批归一化(Batch Norm)和层归一化(Layer Norm)
归一化核心是为了让不同层输入的取值范围或者分布能够比较一致
误差的产生:
本层的输入为上一层的输出,随着参数的更新导致的输入(输出)分布发生改变和深度的加深,差异会越来越大,然而预测的条件分布始终是相同的,就造成了误差
因此需要归一化:归一化为标准正态分布</p>
<p>批归一化是指在一个 mini-batch 上进行归一化，相当于对一个 batch 对样本拆分出来一部分，首先计算样本的均值：
$$
μj=\frac{1}{m}\sum_{i=1}^{m}Z_{ji}
$$
其中，<code>Zji</code> 是样本 i 在第 j 个维度上的值，m 就是 mini-batch 的大小。</p>
<p>再计算样本的方差：
$$
σ2=\frac{1}{m}\sum_{i=1}^{m}(Z_{ji}-\mu_j)^2
$$
最后，对每个样本的值减去均值再除以标准差来将这一个 mini-batch 的样本的分布转化为标准正态分布：
$$
Z~j=\frac{Z_j-\mu_j}{\sqrt{\sigma^2+\epsilon}}
$$
此处加上 <code>ϵ</code> 这一极小量是为了避免分母为0。</p>
<p>但是批归一化存在缺陷.例如:
1.显存有限,mini-batch比较小,批归一化无法返回全局信息,效果比较差
2.对于在时间维度展开的 RNN，不同句子的同一分布大概率不同，所以 Batch 	                     Norm 的归一化会失去意义
3.在训练时，Batch Norm 需要保存每个 step 的统计信息（均值和方差）。在测试时，由于变长句子的特性，测试集可能出现比训练集更长的句子，所以对于后面位   置的 step，是没有训练的统计量使用的
4.应用 Batch Norm，每个 step 都需要去保存和计算 batch 统计量，耗时又耗力</p>
<p>因此出现了层归一化:
Layer Norm 的归一化方式其实和 Batch Norm 是完全一样的，只是统计统计量的维度不同</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">class LayerNorm(nn.Module):
</span></span><span class="line"><span class="cl">    &#39;&#39;&#39; Layer Norm 层&#39;&#39;&#39;
</span></span><span class="line"><span class="cl">    def __init__(self, features, eps=1e-6):
</span></span><span class="line"><span class="cl">        super().__init__()
</span></span><span class="line"><span class="cl">        # 线性矩阵做映射
</span></span><span class="line"><span class="cl">        self.a_2 = nn.Parameter(torch.ones(features))
</span></span><span class="line"><span class="cl">        self.b_2 = nn.Parameter(torch.zeros(features))
</span></span><span class="line"><span class="cl">        self.eps = eps
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    def forward(self, x):
</span></span><span class="line"><span class="cl">        # 在统计每个样本所有维度的值，求均值和方差
</span></span><span class="line"><span class="cl">        mean = x.mean(-1, keepdim=True) # mean: [bsz, max_len, 1]
</span></span><span class="line"><span class="cl">        std = x.std(-1, keepdim=True) # std: [bsz, max_len, 1]
</span></span><span class="line"><span class="cl">        # 注意这里也在最后一个维度发生了广播
</span></span><span class="line"><span class="cl">        return self.a_2 * (x - mean) / (std + self.eps) + self.b_2
</span></span></code></pre></td></tr></table>
</div>
</div><p>2.2.4 残差连接
为了避免模型退化,使用残差连接的思想来连接每一个子层:
即下一层的输入不仅是上一层的输出，还包括上一层的输入.允许最底层信息直接传到最高层,让高层专注残差的学习
例如:
在 Encoder 中，在第一个子层，输入会先进行层归一化（Layer Norm），然后进入多头自注意力层，其输出会与原输入相加。在第二个子层也是一样。</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">x=x+MultiHeadSelfAttention(LayerNorm(x))
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">output=x+FNN(LayerNorm(x))
</span></span></code></pre></td></tr></table>
</div>
</div><p>在代码实现中:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl"># 注意力计算
</span></span><span class="line"><span class="cl">h = x + self.attention.forward(self.attention_norm(x))
</span></span><span class="line"><span class="cl"># 经过前馈神经网络
</span></span><span class="line"><span class="cl">out = h + self.feed_forward.forward(self.fnn_norm(h))
</span></span></code></pre></td></tr></table>
</div>
</div><p>在上文代码中，self.attention_norm 和 self.fnn_norm 都是 LayerNorm 层，self.attn 是注意力层，而 self.feed_forward 是前馈神经网络。</p>
<h3 id="225-encoder">2.2.5 Encoder</h3>
<p>Encoder 由 N 个 Encoder Layer 组成，每一个 Encoder Layer 包括一个注意力层和一个前馈神经网络</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">class EncoderLayer(nn.Module):
</span></span><span class="line"><span class="cl">  &#39;&#39;&#39;Encoder层&#39;&#39;&#39;
</span></span><span class="line"><span class="cl">    def __init__(self, args):
</span></span><span class="line"><span class="cl">        super().__init__()
</span></span><span class="line"><span class="cl">        # 一个 Layer 中有两个 LayerNorm，分别在 Attention 之前和 MLP 之前
</span></span><span class="line"><span class="cl">        self.attention_norm = LayerNorm(args.n_embd)
</span></span><span class="line"><span class="cl">        # Encoder 不需要掩码，传入 is_causal=False
</span></span><span class="line"><span class="cl">        self.attention = MultiHeadAttention(args, is_causal=False)
</span></span><span class="line"><span class="cl">        self.fnn_norm = LayerNorm(args.n_embd)
</span></span><span class="line"><span class="cl">        self.feed_forward = MLP(args.dim, args.dim, args.dropout)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    def forward(self, x):
</span></span><span class="line"><span class="cl">        # Layer Norm
</span></span><span class="line"><span class="cl">        norm_x = self.attention_norm(x)
</span></span><span class="line"><span class="cl">        # 自注意力
</span></span><span class="line"><span class="cl">        h = x + self.attention.forward(norm_x, norm_x, norm_x)
</span></span><span class="line"><span class="cl">        # 经过前馈神经网络
</span></span><span class="line"><span class="cl">        out = h + self.feed_forward.forward(self.fnn_norm(h))
</span></span><span class="line"><span class="cl">        return out
</span></span></code></pre></td></tr></table>
</div>
</div><p>然后我们搭建一个 Encoder，由 N 个 Encoder Layer 组成，在最后会加入一个 Layer Norm 实现规范化:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">class Encoder(nn.Module):
</span></span><span class="line"><span class="cl">    &#39;&#39;&#39;Encoder 块&#39;&#39;&#39;
</span></span><span class="line"><span class="cl">    def __init__(self, args):
</span></span><span class="line"><span class="cl">        super(Encoder, self).__init__() 
</span></span><span class="line"><span class="cl">        # 一个 Encoder 由 N 个 Encoder Layer 组成
</span></span><span class="line"><span class="cl">        self.layers = nn.ModuleList([EncoderLayer(args) for _ in range(args.n_layer)])
</span></span><span class="line"><span class="cl">        self.norm = LayerNorm(args.n_embd)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    def forward(self, x):
</span></span><span class="line"><span class="cl">        &#34;分别通过 N 层 Encoder Layer&#34;
</span></span><span class="line"><span class="cl">        for layer in self.layers:
</span></span><span class="line"><span class="cl">            x = layer(x)
</span></span><span class="line"><span class="cl">        return self.norm(x)
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="226-decoder">2.2.6 Decoder</h3>
<p>我们也可以先搭建 Decoder Layer，再将 N 个 Decoder Layer 组装为 Decoder</p>
<p>Decoder 由两个注意力层和一个前馈神经网络组成。第一个注意力层是一个掩码自注意力层，即使用 Mask 的注意力计算，保证每一个 token 只能使用该 token 之前的注意力分数；第二个注意力层是一个多头注意力层，该层将使用第一个注意力层的输出作为 query，使用 Encoder 的输出作为 key 和 value，来计算注意力分数。最后，再经过前馈神经网络：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">class DecoderLayer(nn.Module):
</span></span><span class="line"><span class="cl">  &#39;&#39;&#39;解码层&#39;&#39;&#39;
</span></span><span class="line"><span class="cl">    def __init__(self, args):
</span></span><span class="line"><span class="cl">        super().__init__()
</span></span><span class="line"><span class="cl">        # 一个 Layer 中有三个 LayerNorm，分别在 Mask Attention 之前、Self Attention 之前和 MLP 之前
</span></span><span class="line"><span class="cl">        self.attention_norm_1 = LayerNorm(args.n_embd)
</span></span><span class="line"><span class="cl">        # Decoder 的第一个部分是 Mask Attention，传入 is_causal=True
</span></span><span class="line"><span class="cl">        self.mask_attention = MultiHeadAttention(args, is_causal=True)
</span></span><span class="line"><span class="cl">        self.attention_norm_2 = LayerNorm(args.n_embd)
</span></span><span class="line"><span class="cl">        # Decoder 的第二个部分是 类似于 Encoder 的 Attention，传入 is_causal=False
</span></span><span class="line"><span class="cl">        self.attention = MultiHeadAttention(args, is_causal=False)
</span></span><span class="line"><span class="cl">        self.ffn_norm = LayerNorm(args.n_embd)
</span></span><span class="line"><span class="cl">        # 第三个部分是 MLP
</span></span><span class="line"><span class="cl">        self.feed_forward = MLP(args.dim, args.dim, args.dropout)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    def forward(self, x, enc_out):
</span></span><span class="line"><span class="cl">        # Layer Norm
</span></span><span class="line"><span class="cl">        norm_x = self.attention_norm_1(x)
</span></span><span class="line"><span class="cl">        # 掩码自注意力
</span></span><span class="line"><span class="cl">        x = x + self.mask_attention.forward(norm_x, norm_x, norm_x)
</span></span><span class="line"><span class="cl">        # 多头注意力
</span></span><span class="line"><span class="cl">        norm_x = self.attention_norm_2(x)
</span></span><span class="line"><span class="cl">        h = x + self.attention.forward(norm_x, enc_out, enc_out)
</span></span><span class="line"><span class="cl">        # 经过前馈神经网络
</span></span><span class="line"><span class="cl">        out = h + self.feed_forward.forward(self.ffn_norm(h))
</span></span><span class="line"><span class="cl">        return out
</span></span></code></pre></td></tr></table>
</div>
</div><p>然后同样的，我们搭建一个 Decoder 块：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">class Decoder(nn.Module):
</span></span><span class="line"><span class="cl">    &#39;&#39;&#39;解码器&#39;&#39;&#39;
</span></span><span class="line"><span class="cl">    def __init__(self, args):
</span></span><span class="line"><span class="cl">        super(Decoder, self).__init__() 
</span></span><span class="line"><span class="cl">        # 一个 Decoder 由 N 个 Decoder Layer 组成
</span></span><span class="line"><span class="cl">        self.layers = nn.ModuleList([DecoderLayer(args) for _ in range(args.n_layer)])
</span></span><span class="line"><span class="cl">        self.norm = LayerNorm(args.n_embd)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    def forward(self, x, enc_out):
</span></span><span class="line"><span class="cl">        &#34;Pass the input (and mask) through each layer in turn.&#34;
</span></span><span class="line"><span class="cl">        for layer in self.layers:
</span></span><span class="line"><span class="cl">            x = layer(x, enc_out)
</span></span><span class="line"><span class="cl">        return self.norm(x)
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="23-搭建一个transformer">2.3 搭建一个Transformer</h2>
<h3 id="231-embedding层">2.3.1 Embedding层</h3>
<p>将自然语言转化为机器能够处理的向量
Embedding:存储固定大小的词典的嵌入向量查找表
在进入神经网络之前.自然语言输入通过tokenizer,将自然语言输入切分成token并且转化成一个固定的index
Embedding的输入:一个形状为 （batch_size，seq_len，1）:
第一个维度是一次批处理的数量，第二个维度是自然语言序列的长度，第三个维度则是 token 经过 tokenizer 转化成的 index 值</p>
<p>而 Embedding 内部其实是一个可训练的（Vocab_size，embedding_dim）的权重矩阵，词表里的每一个值，都对应一行维度为 embedding_dim 的向量。对于输入的值，会对应到这个词向量，然后拼接成（batch_size，seq_len，embedding_dim）的矩阵输出。</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">self.tok_embeddings = nn.Embedding(args.vocab_size, args.dim)
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="223-位置编码">2.2.3 位置编码</h3>
<p>注意计算方式的缺陷:
导致序列中相对位置的丢失
例如:对于序列中的每一个 token，其他各个位置对其来说都是平等的，即“我喜欢你”和“你喜欢我”在注意力机制看来是完全相同的
解决方法:位置编码位置编码，即根据序列中 token 的相对位置对其进行编码，再将位置编码加入词向量编码中。位置编码的方式有很多，Transformer 使用了正余弦函数来进行位置编码（绝对位置编码Sinusoidal），其编码方式为：
$$
PE(pos,2i)=sin(pos/10000
2i/d
model
​</p>
<p>)
$$</p>
<p>$$
PE(pos,2i+1)=cos(pos/10000
2i/d
model
​</p>
<p>)
$$</p>
<p>上式中，pos 为 token 在句子中的位置，2i 和 2i+1 则指示了位置编码向量的维度索引是奇数还是偶数，从上式中我们可以看出对于奇数维度和偶数维度，Transformer 采用了不同的函数进行编码。
例子:</p>
<p>则经过位置编码后的词向量为：</p>
<p>xPE=[0.10.20.30.40.20.30.40.50.30.40.50.60.40.50.60.7]+[sin⁡(0100000)cos⁡(0100000)sin⁡(0100002/4)cos⁡(0100002/4)sin⁡(1100000)cos⁡(1100000)sin⁡(1100002/4)cos⁡(1100002/4)sin⁡(2100000)cos⁡(2100000)sin⁡(2100002/4)cos⁡(2100002/4)sin⁡(3100000)cos⁡(3100000)sin⁡(3100002/4)cos⁡(3100002/4)]=[0.11.20.31.41.0410.840.411.491.209−0.0160.521.590.541−0.4890.8951.655]xPE=0.10.20.30.40.20.30.40.50.30.40.50.60.40.50.60.7+sin(1000000)sin(1000001)sin(1000002)sin(1000003)cos(1000000)cos(1000001)cos(1000002)cos(1000003)sin(100002/40)sin(100002/41)sin(100002/42)sin(100002/43)cos(100002/40)cos(100002/41)cos(100002/42)cos(100002/43)=0.11.0411.2090.5411.20.84−0.016−0.4890.30.410.520.8951.41.491.591.655</p>
<p>我们可以使用如下的代码来获取上述例子的位置编码：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">numpy</span> <span class="k">as</span> <span class="nn">np</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">matplotlib.pyplot</span> <span class="k">as</span> <span class="nn">plt</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">PositionEncoding</span><span class="p">(</span><span class="n">seq_len</span><span class="p">,</span> <span class="n">d_model</span><span class="p">,</span> <span class="n">n</span><span class="o">=</span><span class="mi">10000</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">P</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">zeros</span><span class="p">((</span><span class="n">seq_len</span><span class="p">,</span> <span class="n">d_model</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">k</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">seq_len</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="n">np</span><span class="o">.</span><span class="n">arange</span><span class="p">(</span><span class="nb">int</span><span class="p">(</span><span class="n">d_model</span><span class="o">/</span><span class="mi">2</span><span class="p">)):</span>
</span></span><span class="line"><span class="cl">            <span class="n">denominator</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">power</span><span class="p">(</span><span class="n">n</span><span class="p">,</span> <span class="mi">2</span><span class="o">*</span><span class="n">i</span><span class="o">/</span><span class="n">d_model</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">P</span><span class="p">[</span><span class="n">k</span><span class="p">,</span> <span class="mi">2</span><span class="o">*</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">sin</span><span class="p">(</span><span class="n">k</span><span class="o">/</span><span class="n">denominator</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">P</span><span class="p">[</span><span class="n">k</span><span class="p">,</span> <span class="mi">2</span><span class="o">*</span><span class="n">i</span><span class="o">+</span><span class="mi">1</span><span class="p">]</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">cos</span><span class="p">(</span><span class="n">k</span><span class="o">/</span><span class="n">denominator</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">P</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">P</span> <span class="o">=</span> <span class="n">PositionEncoding</span><span class="p">(</span><span class="n">seq_len</span><span class="o">=</span><span class="mi">4</span><span class="p">,</span> <span class="n">d_model</span><span class="o">=</span><span class="mi">4</span><span class="p">,</span> <span class="n">n</span><span class="o">=</span><span class="mi">100</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">P</span><span class="p">)</span><span class="n">Copy</span> <span class="n">to</span> <span class="n">clipboardErrorCopied</span>
</span></span><span class="line"><span class="cl"><span class="p">[[</span> <span class="mf">0.</span>          <span class="mf">1.</span>          <span class="mf">0.</span>          <span class="mf">1.</span>        <span class="p">]</span>
</span></span><span class="line"><span class="cl"> <span class="p">[</span> <span class="mf">0.84147098</span>  <span class="mf">0.54030231</span>  <span class="mf">0.09983342</span>  <span class="mf">0.99500417</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"> <span class="p">[</span> <span class="mf">0.90929743</span> <span class="o">-</span><span class="mf">0.41614684</span>  <span class="mf">0.19866933</span>  <span class="mf">0.98006658</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"> <span class="p">[</span> <span class="mf">0.14112001</span> <span class="o">-</span><span class="mf">0.9899925</span>   <span class="mf">0.29552021</span>  <span class="mf">0.95533649</span><span class="p">]]</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>这样的位置编码主要有两个好处：</p>
<ol>
<li>使 PE 能够适应比训练集里面所有句子更长的句子，假设训练集里面最长的句子是有 20 个单词，突然来了一个长度为 21 的句子，则使用公式计算的方法可以计算出第 21 位的 Embedding。</li>
<li>可以让模型容易地计算出相对位置，对于固定长度的间距 k，PE(pos+k) 可以用 PE(pos) 计算得到。因为 Sin(A+B) = Sin(A)Cos(B) + Cos(A)Sin(B), Cos(A+B) = Cos(A)Cos(B) - Sin(A)Sin(B)。</li>
</ol>
<!-- 原文此处有两张位置编码可视化图（Typora 本地图片，发布版移除；如需补图请导出到 assets/img 后恢复） -->
<p>上面那个好复杂,还是代码看着顺眼</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">PositionalEncoding</span><span class="p">(</span><span class="n">nn</span><span class="o">.</span><span class="n">Module</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="s1">&#39;&#39;&#39;位置编码模块&#39;&#39;&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">args</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="nb">super</span><span class="p">(</span><span class="n">PositionalEncoding</span><span class="p">,</span> <span class="bp">self</span><span class="p">)</span><span class="o">.</span><span class="fm">__init__</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Dropout 层</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># self.dropout = nn.Dropout(p=args.dropout)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1"># block size 是序列的最大长度</span>
</span></span><span class="line"><span class="cl">        <span class="n">pe</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">zeros</span><span class="p">(</span><span class="n">args</span><span class="o">.</span><span class="n">block_size</span><span class="p">,</span> <span class="n">args</span><span class="o">.</span><span class="n">n_embd</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">position</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">arange</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">args</span><span class="o">.</span><span class="n">block_size</span><span class="p">)</span><span class="o">.</span><span class="n">unsqueeze</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># 计算 theta</span>
</span></span><span class="line"><span class="cl">        <span class="n">div_term</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">exp</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="n">torch</span><span class="o">.</span><span class="n">arange</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">args</span><span class="o">.</span><span class="n">n_embd</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span> <span class="o">*</span> <span class="o">-</span><span class="p">(</span><span class="n">math</span><span class="o">.</span><span class="n">log</span><span class="p">(</span><span class="mf">10000.0</span><span class="p">)</span> <span class="o">/</span> <span class="n">args</span><span class="o">.</span><span class="n">n_embd</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># 分别计算 sin、cos 结果</span>
</span></span><span class="line"><span class="cl">        <span class="n">pe</span><span class="p">[:,</span> <span class="mi">0</span><span class="p">::</span><span class="mi">2</span><span class="p">]</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">sin</span><span class="p">(</span><span class="n">position</span> <span class="o">*</span> <span class="n">div_term</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">pe</span><span class="p">[:,</span> <span class="mi">1</span><span class="p">::</span><span class="mi">2</span><span class="p">]</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">cos</span><span class="p">(</span><span class="n">position</span> <span class="o">*</span> <span class="n">div_term</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">pe</span> <span class="o">=</span> <span class="n">pe</span><span class="o">.</span><span class="n">unsqueeze</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">register_buffer</span><span class="p">(</span><span class="s2">&#34;pe&#34;</span><span class="p">,</span> <span class="n">pe</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">forward</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">x</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># 将位置编码加到 Embedding 结果上</span>
</span></span><span class="line"><span class="cl">        <span class="n">x</span> <span class="o">=</span> <span class="n">x</span> <span class="o">+</span> <span class="bp">self</span><span class="o">.</span><span class="n">pe</span><span class="p">[:,</span> <span class="p">:</span> <span class="n">x</span><span class="o">.</span><span class="n">size</span><span class="p">(</span><span class="mi">1</span><span class="p">)]</span><span class="o">.</span><span class="n">requires_grad_</span><span class="p">(</span><span class="kc">False</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">x</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="233-一个完整的transformer">2.3.3 一个完整的Transformer</h3>
<img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/2-figures/3-1.png" alt="图片描述" style="zoom:50%;" />
<p>但需要注意的是，上图是原论文《Attention is all you need》配图，LayerNorm 层放在了 Attention 层后面，也就是“Post-Norm”结构，但在其发布的源代码中，LayerNorm 层是放在 Attention 层前面的，也就是“Pre Norm”结构。考虑到目前 LLM 一般采用“Pre-Norm”结构（可以使 loss 更稳定），本文在实现时采用“Pre-Norm”结构。</p>
<p>如图，经过 tokenizer 映射后的输出先经过 Embedding 层和 Positional Embedding 层编码，然后进入上一节讲过的 N 个 Encoder 和 N 个 Decoder（在 Transformer 原模型中，N 取为6），最后经过一个线性层和一个 Softmax 层就得到了最终输出。</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span><span class="lnt">32
</span><span class="lnt">33
</span><span class="lnt">34
</span><span class="lnt">35
</span><span class="lnt">36
</span><span class="lnt">37
</span><span class="lnt">38
</span><span class="lnt">39
</span><span class="lnt">40
</span><span class="lnt">41
</span><span class="lnt">42
</span><span class="lnt">43
</span><span class="lnt">44
</span><span class="lnt">45
</span><span class="lnt">46
</span><span class="lnt">47
</span><span class="lnt">48
</span><span class="lnt">49
</span><span class="lnt">50
</span><span class="lnt">51
</span><span class="lnt">52
</span><span class="lnt">53
</span><span class="lnt">54
</span><span class="lnt">55
</span><span class="lnt">56
</span><span class="lnt">57
</span><span class="lnt">58
</span><span class="lnt">59
</span><span class="lnt">60
</span><span class="lnt">61
</span><span class="lnt">62
</span><span class="lnt">63
</span><span class="lnt">64
</span><span class="lnt">65
</span><span class="lnt">66
</span><span class="lnt">67
</span><span class="lnt">68
</span><span class="lnt">69
</span><span class="lnt">70
</span><span class="lnt">71
</span><span class="lnt">72
</span><span class="lnt">73
</span><span class="lnt">74
</span><span class="lnt">75
</span><span class="lnt">76
</span><span class="lnt">77
</span><span class="lnt">78
</span><span class="lnt">79
</span><span class="lnt">80
</span><span class="lnt">81
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">Transformer</span><span class="p">(</span><span class="n">nn</span><span class="o">.</span><span class="n">Module</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">   <span class="s1">&#39;&#39;&#39;整体模型&#39;&#39;&#39;</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">args</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="fm">__init__</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># 必须输入词表大小和 block size</span>
</span></span><span class="line"><span class="cl">        <span class="k">assert</span> <span class="n">args</span><span class="o">.</span><span class="n">vocab_size</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span>
</span></span><span class="line"><span class="cl">        <span class="k">assert</span> <span class="n">args</span><span class="o">.</span><span class="n">block_size</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">args</span> <span class="o">=</span> <span class="n">args</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">transformer</span> <span class="o">=</span> <span class="n">nn</span><span class="o">.</span><span class="n">ModuleDict</span><span class="p">(</span><span class="nb">dict</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="n">wte</span> <span class="o">=</span> <span class="n">nn</span><span class="o">.</span><span class="n">Embedding</span><span class="p">(</span><span class="n">args</span><span class="o">.</span><span class="n">vocab_size</span><span class="p">,</span> <span class="n">args</span><span class="o">.</span><span class="n">n_embd</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">            <span class="n">wpe</span> <span class="o">=</span> <span class="n">PositionalEncoding</span><span class="p">(</span><span class="n">args</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">            <span class="n">drop</span> <span class="o">=</span> <span class="n">nn</span><span class="o">.</span><span class="n">Dropout</span><span class="p">(</span><span class="n">args</span><span class="o">.</span><span class="n">dropout</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">            <span class="n">encoder</span> <span class="o">=</span> <span class="n">Encoder</span><span class="p">(</span><span class="n">args</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">            <span class="n">decoder</span> <span class="o">=</span> <span class="n">Decoder</span><span class="p">(</span><span class="n">args</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="p">))</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># 最后的线性层，输入是 n_embd，输出是词表大小</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">lm_head</span> <span class="o">=</span> <span class="n">nn</span><span class="o">.</span><span class="n">Linear</span><span class="p">(</span><span class="n">args</span><span class="o">.</span><span class="n">n_embd</span><span class="p">,</span> <span class="n">args</span><span class="o">.</span><span class="n">vocab_size</span><span class="p">,</span> <span class="n">bias</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1"># 初始化所有的权重</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">apply</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_init_weights</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1"># 查看所有参数的数量</span>
</span></span><span class="line"><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;number of parameters: </span><span class="si">%.2f</span><span class="s2">M&#34;</span> <span class="o">%</span> <span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">get_num_params</span><span class="p">()</span><span class="o">/</span><span class="mf">1e6</span><span class="p">,))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="s1">&#39;&#39;&#39;统计所有参数的数量&#39;&#39;&#39;</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">get_num_params</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">non_embedding</span><span class="o">=</span><span class="kc">False</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># non_embedding: 是否统计 embedding 的参数</span>
</span></span><span class="line"><span class="cl">        <span class="n">n_params</span> <span class="o">=</span> <span class="nb">sum</span><span class="p">(</span><span class="n">p</span><span class="o">.</span><span class="n">numel</span><span class="p">()</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">parameters</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># 如果不统计 embedding 的参数，就减去</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">non_embedding</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">n_params</span> <span class="o">-=</span> <span class="bp">self</span><span class="o">.</span><span class="n">transformer</span><span class="o">.</span><span class="n">wte</span><span class="o">.</span><span class="n">weight</span><span class="o">.</span><span class="n">numel</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">n_params</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="s1">&#39;&#39;&#39;初始化权重&#39;&#39;&#39;</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">_init_weights</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">module</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># 线性层和 Embedding 层初始化为正则分布</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">module</span><span class="p">,</span> <span class="n">nn</span><span class="o">.</span><span class="n">Linear</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="n">torch</span><span class="o">.</span><span class="n">nn</span><span class="o">.</span><span class="n">init</span><span class="o">.</span><span class="n">normal_</span><span class="p">(</span><span class="n">module</span><span class="o">.</span><span class="n">weight</span><span class="p">,</span> <span class="n">mean</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span> <span class="n">std</span><span class="o">=</span><span class="mf">0.02</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="n">module</span><span class="o">.</span><span class="n">bias</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                <span class="n">torch</span><span class="o">.</span><span class="n">nn</span><span class="o">.</span><span class="n">init</span><span class="o">.</span><span class="n">zeros_</span><span class="p">(</span><span class="n">module</span><span class="o">.</span><span class="n">bias</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">elif</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">module</span><span class="p">,</span> <span class="n">nn</span><span class="o">.</span><span class="n">Embedding</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="n">torch</span><span class="o">.</span><span class="n">nn</span><span class="o">.</span><span class="n">init</span><span class="o">.</span><span class="n">normal_</span><span class="p">(</span><span class="n">module</span><span class="o">.</span><span class="n">weight</span><span class="p">,</span> <span class="n">mean</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span> <span class="n">std</span><span class="o">=</span><span class="mf">0.02</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="s1">&#39;&#39;&#39;前向计算函数&#39;&#39;&#39;</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">forward</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">idx</span><span class="p">,</span> <span class="n">targets</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># 输入为 idx，维度为 (batch size, sequence length, 1)；targets 为目标序列，用于计算 loss</span>
</span></span><span class="line"><span class="cl">        <span class="n">device</span> <span class="o">=</span> <span class="n">idx</span><span class="o">.</span><span class="n">device</span>
</span></span><span class="line"><span class="cl">        <span class="n">b</span><span class="p">,</span> <span class="n">t</span> <span class="o">=</span> <span class="n">idx</span><span class="o">.</span><span class="n">size</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="k">assert</span> <span class="n">t</span> <span class="o">&lt;=</span> <span class="bp">self</span><span class="o">.</span><span class="n">args</span><span class="o">.</span><span class="n">block_size</span><span class="p">,</span> <span class="sa">f</span><span class="s2">&#34;不能计算该序列，该序列长度为 </span><span class="si">{</span><span class="n">t</span><span class="si">}</span><span class="s2">, 最大序列长度只有 </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">args</span><span class="o">.</span><span class="n">block_size</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1"># 通过 self.transformer</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># 首先将输入 idx 通过 Embedding 层，得到维度为 (batch size, sequence length, n_embd)</span>
</span></span><span class="line"><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;idx&#34;</span><span class="p">,</span><span class="n">idx</span><span class="o">.</span><span class="n">size</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># 通过 Embedding 层</span>
</span></span><span class="line"><span class="cl">        <span class="n">tok_emb</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">transformer</span><span class="o">.</span><span class="n">wte</span><span class="p">(</span><span class="n">idx</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;tok_emb&#34;</span><span class="p">,</span><span class="n">tok_emb</span><span class="o">.</span><span class="n">size</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># 然后通过位置编码</span>
</span></span><span class="line"><span class="cl">        <span class="n">pos_emb</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">transformer</span><span class="o">.</span><span class="n">wpe</span><span class="p">(</span><span class="n">tok_emb</span><span class="p">)</span> 
</span></span><span class="line"><span class="cl">        <span class="c1"># 再进行 Dropout</span>
</span></span><span class="line"><span class="cl">        <span class="n">x</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">transformer</span><span class="o">.</span><span class="n">drop</span><span class="p">(</span><span class="n">pos_emb</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># 然后通过 Encoder</span>
</span></span><span class="line"><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;x after wpe:&#34;</span><span class="p">,</span><span class="n">x</span><span class="o">.</span><span class="n">size</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">        <span class="n">enc_out</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">transformer</span><span class="o">.</span><span class="n">encoder</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;enc_out:&#34;</span><span class="p">,</span><span class="n">enc_out</span><span class="o">.</span><span class="n">size</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># 再通过 Decoder</span>
</span></span><span class="line"><span class="cl">        <span class="n">x</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">transformer</span><span class="o">.</span><span class="n">decoder</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">enc_out</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;x after decoder:&#34;</span><span class="p">,</span><span class="n">x</span><span class="o">.</span><span class="n">size</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">targets</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="c1"># 训练阶段，如果我们给了 targets，就计算 loss</span>
</span></span><span class="line"><span class="cl">            <span class="c1"># 先通过最后的 Linear 层，得到维度为 (batch size, sequence length, vocab size)</span>
</span></span><span class="line"><span class="cl">            <span class="n">logits</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">lm_head</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="c1"># 再跟 targets 计算交叉熵</span>
</span></span><span class="line"><span class="cl">            <span class="n">loss</span> <span class="o">=</span> <span class="n">F</span><span class="o">.</span><span class="n">cross_entropy</span><span class="p">(</span><span class="n">logits</span><span class="o">.</span><span class="n">view</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="n">logits</span><span class="o">.</span><span class="n">size</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">)),</span> <span class="n">targets</span><span class="o">.</span><span class="n">view</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">),</span> <span class="n">ignore_index</span><span class="o">=-</span><span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="c1"># 推理阶段，我们只需要 logits，loss 为 None</span>
</span></span><span class="line"><span class="cl">            <span class="c1"># 取 -1 是只取序列中的最后一个作为输出</span>
</span></span><span class="line"><span class="cl">            <span class="n">logits</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">lm_head</span><span class="p">(</span><span class="n">x</span><span class="p">[:,</span> <span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">],</span> <span class="p">:])</span> <span class="c1"># note: using list [-1] to preserve the time dim</span>
</span></span><span class="line"><span class="cl">            <span class="n">loss</span> <span class="o">=</span> <span class="kc">None</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">logits</span><span class="p">,</span> <span class="n">loss</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>在前向计算函数中，我们对模型使用 pytorch 的交叉熵函数来计算损失</p>
]]></content:encoded>
    </item>
    <item>
      <title>LLM · 预训练语言模型（PLM）</title>
      <link>https://tuanzi-wow.cn/llm/llm-pretrain-plm/</link>
      <pubDate>Sun, 06 Sep 2026 00:00:00 +0800</pubDate>
      <guid>https://tuanzi-wow.cn/llm/llm-pretrain-plm/</guid>
      <description>PLM 三大范式：BERT 的 MLM&#43;NSP、RoBERTa/ALBERT 的改进、T5 的 Encoder-Decoder 与 GPT/LLaMA/GLM 的 Decoder-Only。</description>
      <content:encoded><![CDATA[<h1 id="第三章-预训练语言模型plm">第三章 预训练语言模型(PLM)</h1>
<h2 id="31--encoder-only-plm">3.1  Encoder-only PLM</h2>
<h3 id="311-bert-bidirectional-encoder-representations-from-transformers">3.1.1 BERT( Bidirectional Encoder Representations from Transformers)</h3>
<p>由Google发布的,引领了预训练+微调的的潮流</p>
<h4 id="1思想沿承">(1)思想沿承</h4>
<p>统一了多种思想的预训练框架,包括:
<strong>Transformer架构:</strong>
通过在Transformer的模型基座上进行优化,将Encoder的结构进行堆叠,扩大模型参数
<strong>预训练+微调范式:</strong>
ELMo模型基于LSTM双向架构
而BERT则将模型框架调整为了Transformer,引入了更适合文本理解,能捕捉深层双向语义的预训练任务MLM</p>
<p>接下来从模型架构,预训练任务,下游任务来深入剖析BERT</p>
<h4 id="2模型架构">(2)模型架构</h4>
<p>采取了将Transformer的Encoder部分堆叠而成</p>
<p><a href="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/3-figures/1-0.png">图片描述</a></p>
<p>其输入一般是文本序列,输出一般是Label</p>
<p>分类头:predictions_heads:
将多维度的隐藏状态从线性层转化到分类维度</p>
<p>模型整体上是由Embedding,Encoder,prediction_heads组成的</p>
<p><a href="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/3-figures/1-1.png">图片描述</a></p>
<p>输入文本-&gt;tokenizer-&gt;input_ids-&gt;Embedding-&gt;hidden_states-&gt;Encoder-&gt;最顶层hidden_states-&gt;predictions_heads
其中Encoder块中,BERT由有两种规模的模型,分别是:
base版本:12层Encoder layer 768的隐藏层维度 总参数量110M
lager版本:24层Encoder layer 1024隐藏层维度从总参数数量340M</p>
<p>BERT的分词方法: WordPiece
基于统计的子词切分算法,核心在于把单词拆解为子词,其合并操作的依据所示最大化语言模型的似然度,对于中文等非空格分割的语言通常采用单个汉字作为token处理</p>
<p>prediction_heads其实就是线性层加上激活函数</p>
<p><a href="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/3-figures/1-5.png">图片描述</a></p>
<p>而每一层Encoder layer都是和Transformer中的Encoder结构相似的层
<a href="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/3-figures/1-2.png">图片描述</a></p>
<p>通过 Embedding 层映射的 hidden_states 进入核心的 attention 机制，然后通过残差连接的机制和原输入相加，再经过一层 Intermediate 层得到最终输出</p>
<p>采用的激活函数:GELU函数,全名为高斯误差线性单元激活函数
起计算方式为:
$$
GELU(x)=0.5x(1+tanh(
π/
2
​</p>
<p>​
)(x+0.044715x
3
))
$$
核心思路L将随机正则的思想引入激活函数,通过输入自身的概率分布,来决定抛弃还是保留自己的神经元</p>
<p>BERT的注意力机制和Transformer几乎完全一致,但是BERT的相对位置编码融合在了记忆力机制中,将相对位置编码同样是为可以训练的权重参数
<a href="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/3-figures/1-4.png">图片描述</a></p>
<p>BERT 的注意力计算过程和 Transformer 的唯一差异在于，在完成注意力分数的计算之后，先通过 Position Embedding 层来融入相对位置信息。这里的 Position Embedding 层，其实就是一层线性矩阵。通过可训练的参数来拟合相对位置，相对而言比 Transformer 使用的绝对位置编码 Sinusoidal 能够拟合更丰富的相对位置信息，但是，这样也增加了不少模型参数，同时完全无法处理超过模型训练长度的输入
注解:最开始BERT采用的也是类似于Transformer的绝对位置编码,后续改进为相对位置编码</p>
<h4 id="3预训练任务mlmnsp">(3)预训练任务&ndash;MLM+NSP</h4>
<p>创新点出现了!:新的两个预训练任务MLM+NSP(Next Sentence Prediction,下一句预测)
预训练+微调的范式的优越性在于:
完成预训练的模型可以仅通过微调应用在所有的下游任务当中
因此:可以进一步扩大模型参数和预训练数据量让模型拟合潜在语义和底层知识</p>
<p>预训练数据的核心要求是需要极大的数据规模.通过人工标注产出的全监督数据很难达到这个规模。因此，预训练数据一定是从无监督的语料中获取</p>
<p>这也是为什么传统的预训练任务都是 LM 的原因——LM 使用上文预测下文的方式可以直接应用到任何文本中，对于任意文本，我们只需要将下文遮蔽将上文输入模型要求其预测就可以实现 LM 训练，因此互联网上所有文本语料都可以被用于预训练</p>
<p>LM预训练任务的缺陷:
直接拟合从左到右的语义关系.而忽略了双向的语音关系哦</p>
<p>因此,引入了MLM,第一个M的含义是MASK(掩码)
其思路为:在文本序列中随机遮蔽部分token,将没有被遮蔽的token输入模型中,要求模型根据输入预测被遮蔽的token.
可以同时利用上文和下文一起理解语义来预测被遮蔽的token</p>
<p>MLM的固有缺陷:
训练和下游任务应该是保持一致的,但是MLM不同,下游任务微调和推理的时候,是不存在我们人工加入<MASK>的
预训练和微调不一致会严重影响下游任务微调的性能</p>
<p>MLM的策略改进:(我去真的好聪明)
选择训练语料中的15%进行遮蔽,其中并不是全部都进行遮蔽,其中
80%进行遮蔽:进行MASK训练
10%被替换为任意一个token:让模型无法确定需要预测的token,从而保持每一个
token的上下文表征分布
10%保持不变:消除预训练和微调的不一致
且由于10%随机替换的概率很低,并不会影响模型实质语言理解能力</p>
<p>NSP:
核心思想是针对句级的NLU任务</p>
<p>自然语言推理是指，输入一个前提和一个推理，判断推理是否是符合前提的。这样的任务都需要模型在句级去拟合关系，判断两个句子之间的关系，而不仅是 MLM 在 token 级拟合的语义关系</p>
<p>NSP的核心思路是要求模型判断一个句对的两个句子是否是连续的上下文.例如:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">输入：
</span></span><span class="line"><span class="cl">    Sentence A：I love you.
</span></span><span class="line"><span class="cl">    Sentence B: Because you are wonderful.
</span></span><span class="line"><span class="cl">输出：
</span></span><span class="line"><span class="cl">    1（是连续上下文）
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">输入：
</span></span><span class="line"><span class="cl">    Sentence A：I love you.
</span></span><span class="line"><span class="cl">    Sentence B: Because today&#39;s dinner is so nice.
</span></span><span class="line"><span class="cl">输出：
</span></span><span class="line"><span class="cl">    0（不是连续上下文）
</span></span></code></pre></td></tr></table>
</div>
</div><p>通过要求模型判断句对关系,迫使模型拟合句子之间的关系,来适配句级的NLU任务
正样本可以从无监督语料中抽取连续的句子,负样本对句子打乱后随机抽取,因此可以拥有几乎无限的训练数据</p>
<h4 id="4下游任务微调">(4)下游任务微调</h4>
<p>预训练-微调的重点在于:
通过预训练获得的强大能力是否通过低成本的微调快速迁移到对应的下游任务中</p>
<p>策略:
设计了更加通用的输入和输出层来设配多任务下的迁移学习:
对于每一个输入的文本序列,会在首部加入一个特殊的token代表整句的状态,也就是句级的语义特征.在进行NSP预训练时,就使用该token对应的特征向量来作为最后分类器的输入</p>
<p>在完成预训练后，针对每一个下游任务，只需要使用一定量的全监督人工标注数据，对预训练的 BERT 在该任务上进行微调即可。</p>
<p>对于某些特定、训练数据丰富且强调高吞吐的任务，BERT 比 LLM 更具有可用性</p>
<h3 id="312-roberta">3.1.2 RoBERTa</h3>
<p>经过BERT优化而来的</p>
<h4 id="1优化一-去掉nsp预训练任务">(1)优化一: 去掉NSP预训练任务</h4>
<p>RoBERTa使用BERT的lager框架
实验结果表明:NSP 任务并不能提高模型性能，因为其太过简单，加入到预训练中并不能使下游任务微调时明显受益，甚至会带来负面效果。</p>
<p>对于MLM任务的优化:
动态遮蔽策略:
RoBERTa 对 MLM 任务本身也做出了改进。在 BERT 中，Mask 的操作是在数据处理的阶段完成的，因此后期预训练时同一个 sample 待预测的 <code>&lt;MASK&gt;</code> 总是一致的。由于 BERT 共训练了 40 个 Epoch，为使模型的训练数据更加广泛，BERT 将数据进行了四次随机 Mask，也就是每 10个 Epoch 模型训练的数据是完全一致的。而 RoBERTa 将 Mask 操作放到了训练阶段，也就是动态遮蔽策略，从而让每一个 Epoch 的训练数据 Mask 的位置都不一致。</p>
<p>训练效果优势不明显,但是高效且易于实现,后续更多都采用动态遮蔽</p>
<h4 id="2优化二-更大的bpe词表">(2)优化二: 更大的bpe词表</h4>
<p>Tokenizer的编码策略不同:使用了BPE的编码策略, 即Byte Pair Encoding
字节对编码,以子词对作为分词的单位,例如:
对“Hello World”这句话，可能会切分为“Hel，lo，Wor，ld”四个子词对。而对于以字为基本单位的中文，一般会按照字节编码进行切分。例如，在 UTF-8 编码中，“我”会被编码为“E68891”，那么在 BPE 中可能就会切分成“E68”，“891”两个字词对</p>
<p>BPE编码的词典越大,编码效果越好,在Embedding将其映射到隐藏空间的时候,也会带来模型参数的增加</p>
<p>它的成功证明了更大的预训练数据和更大的预训练补偿的重要意义,这也是LLM诞生的基础之一</p>
<h3 id="313-albert">3.1.3 ALBERT</h3>
<p>优化角度:减小模型参数同时保持模型能力
改进角度:对模型结构进行优化并且对NSP预训练任务进行了改进-&gt;SOP</p>
<h4 id="1优化一将embedding参数进行分解">(1)优化一:将Embedding参数进行分解</h4>
<p>一个方面:参数太大会造成极大的模型计算开销
零一个方面:从Word2Vec的成功来看,此项两并不需要很大的维度
优化方式:
在Embedding层的输出维度和隐藏层解绑,在中间加入一个线性矩阵进行维度变换
参数变化:Embedding的参数从V×H变化到V×E + E×H</p>
<h4 id="2优化二跨层进行参数共享">(2)优化二:跨层进行参数共享</h4>
<p>通过共享参数降低模型的参数,同时可以极大程度地扩大隐藏层维度
实际上:ALNERT仅初始化一个Encoder层,24次运算均经过这一个Encoder层,可以实现一个更宽但是参数量更小地模型</p>
<p>缺陷:训练效率仅略优于BERT,且推理速度还要更慢</p>
<h4 id="3优化三-提出了sop预训练任务">(3)优化三: 提出了SOP预训练任务</h4>
<p>认为NSP过于简单,于是改进为SOP预训练
SOP任务的改进:</p>
<p>正例:由两个连续句子组成
负例:将这两个句子的顺序反过来
迫使学习句子的拟合关系和顺序关系</p>
<p>eg:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">输入：
</span></span><span class="line"><span class="cl">    Sentence A：I love you.
</span></span><span class="line"><span class="cl">    Sentence B: Because you are wonderful.
</span></span><span class="line"><span class="cl">输出：
</span></span><span class="line"><span class="cl">    1（正样本）
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">输入：
</span></span><span class="line"><span class="cl">    Sentence A：Because you are wonderful.
</span></span><span class="line"><span class="cl">    Sentence B: I love you.
</span></span><span class="line"><span class="cl">输出：
</span></span><span class="line"><span class="cl">    0（负样本）
</span></span></code></pre></td></tr></table>
</div>
</div><p>很多模型都是在BERT的架构上进行优化</p>
<h2 id="32-encoder-decoder-plm">3.2 Encoder-Decoder PLM</h2>
<h3 id="321-t5">3.2.1 T5</h3>
<p>T5(Text-To-Text Transfer Transformer)
<strong>基本架构:</strong>
T5 基于 Transformer 架构，包含编码器和解码器两个部分，使用自注意力机制和多头注意力捕捉全局依赖关系，利用相对位置编码处理长序列中的位置信息，并在每层中包含前馈神经网络进一步处理特征
T5 的大一统思想将不同的 NLP 任务如文本分类、问答、翻译等统一表示为输入文本到输出文本的转换
优点:
这种方法简化了模型设计、参数共享和训练过程，提高了模型的泛化能力和效率。通过这种统一处理方式，T5不仅减少了任务特定的模型调试工作，还能够使用相同的数据处理和训练框架，极大地提升了多任务学习的性能和应用的便捷性</p>
<h4 id="1模型结构-encoder-decoder">(1)模型结构: Encoder-Decoder</h4>
<p>编码器用于处理输入文本,解码器用于生成输出文本,两者之间通过注意力机制进行信息交互
主要结构如图:
<a href="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/3-figures/2-1.png">图片描述</a></p>
<p>从整体来看 T5 的模型结构包括 Tokenizer 部分和 Transformer 部分。Tokenizer 部分主要负责将输入文本转换为模型可接受的输入格式，包括分词、编码等操作。Transformer 部分又分为 EncoderLayers 和 DecoderLayers 两部分，他们分别由一个个小的 Block组成，每个 Block 包含了多头注意力机制、前馈神经网络和 Norm 层。</p>
<p>T5 模型的 Encoder 和 Decoder 部分都是基于 Transformer 架构设计的
Encoder-Decoder Attention 仅仅在位置编码和 Mask 机制上有所不同，主要是为了区分输入和输出序列。</p>
<p>于原始Transformer不同的,T5的LayerNorm采用了RMSNorm,计算每一个神经元的均方根来归一化每个隐藏层的激活值
优点:更简单,只有一个可学参数,可以更好地适应不同地任务和数据集
RMS函数可以用下面地数学公式表示:
$$
$$
\mathrm{RMSNorm}(x) = \frac{x}{\sqrt{\frac{1}{n}\sum_{i=1}^{n}x_i^2 + \epsilon}} \cdot \gamma
$$
$$
其中：</p>
<ul>
<li>xi 是输入向量的第 <em>i</em> 个元素</li>
<li>γ* 是可学习的缩放参数</li>
<li>n*n 是输入向量的维度数量</li>
<li>ϵ* 是一个小常数，用于数值稳定性（以避免除以零的情况）</li>
</ul>
<h4 id="2预训练任务">(2)预训练任务</h4>
<p>训练所使用的数据集是一个大规模的文本数据集，包含了各种各样的文本数据，如维基百科、新闻、书籍等等。对数据经过细致的处理后，生成了用于训练的750GB 的数据集 C4，且已在 TensorflowData 中开源</p>
<p>T5的预训练过程如下:
预训练任务:MLM,也成为BERT-style目标:在输入文本中随机遮蔽15%的token
输入格式:将将输入文本替换为&quot;从文本到文本&quot;格式
对于一个给定的文本序列:随机选择一些token进行遮蔽,用特殊占位符替换,被遮蔽的token序列作为输出目标
预训练数据集:C4:提取了大量干净的英语文本,经过了一定程度的清洗,去除了无意义的文本,重复的文本
多任务预训练:尝试将多个任务混合在一起进行预训练
预训练到微调的转换:微调在特定的数据集上进行训练并且根据任务调整编码策略</p>
<h4 id="3-大一统思想">(3) 大一统思想</h4>
<p>所有的NLP任务都可以统一为文本到文本的任务
其设计理念:将不同类型的NLP任务都转化统一的格式:输入和输出都是文本</p>
<p>对于不同的NLP任务，每次输入前都会加上一个任务描述前缀，明确指定当前任务的类型。这不仅帮助模型在预训练阶段学习到不同任务之间的通用特征，也便于在微调阶段迅速适应具体任务</p>
<h2 id="33-decoder-only-plm">3.3 Decoder-only PLM</h2>
<p>目前LLM的基础框架(除了RWKV,Mamba等非Transformer框架除外)
而目前作为开源 LLM 基本架构的 LLaMA 模型，也正是在 GPT 的模型架构基础上优化发展而来</p>
<h4 id="331-gptgenerative-pre-training-language-model">3.3.1 GPT(Generative Pre-Training Language Model)</h4>
<p>GPT的传奇故事:上市了的O/还是那个O/吗?
GPT，即 Generative Pre-Training Language Model，是由 OpenAI 团队于 2018年发布的预训练语言模型。虽然学界普遍认可 BERT 作为预训练语言模型时代的代表，但首先明确提出预训练-微调思想的模型其实是 GPT。GPT 提出了通用预训练的概念，也就是在海量无监督语料上预训练，进而在每个特定任务上进行微调，从而实现这些任务的巨大收益。虽然在发布之初，由于性能略输于不久后发布的 BERT，没能取得轰动性成果，也没能让 GPT 所使用的 Decoder-Only 架构成为学界研究的主流，但 OpenAI 团队坚定地选择了不断扩大预训练数据、增加模型参数，在 GPT 架构上不断优化，最终在 2020年发布的 GPT-3 成就了 LLM 时代的基础，并以 GPT-3 为基座模型的 ChatGPT 成功打开新时代的大门，成为 LLM 时代的最强竞争者也是目前的最大赢家。</p>
<h4 id="1模型架构decoder-only">(1)模型架构&ndash;Decoder Only</h4>
<p>如图:</p>
<p><a href="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/3-figures/3-0.png">alt text</a></p>
<p>选择Decoder进行模型框架的堆叠,也天生更适用于文本生成任务
更加契合NLG任务和Seq2Seq任务
过程:
自然语言输入-&gt;tokenizer-&gt;inputs_ids-&gt;</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">自然语言输入
</span></span><span class="line"><span class="cl">  --&gt; tokenizer
</span></span><span class="line"><span class="cl">input_ids
</span></span><span class="line"><span class="cl">  --&gt; Embedding 层
</span></span><span class="line"><span class="cl">  --&gt; Positional Embedding（Sinusoidal 绝对位置编码）
</span></span><span class="line"><span class="cl">hidden_states
</span></span><span class="line"><span class="cl">  --&gt; Decoder 层 × 12
</span></span><span class="line"><span class="cl">        --&gt; LayerNorm
</span></span><span class="line"><span class="cl">        --&gt; 带掩码的自注意力层（Masked Self-Attention）
</span></span><span class="line"><span class="cl">              --&gt; 通过参数矩阵生成 Query、Key、Value
</span></span><span class="line"><span class="cl">              --&gt; 注意力权重计算
</span></span><span class="line"><span class="cl">              --&gt; 掩码矩阵遮蔽未来 token
</span></span><span class="line"><span class="cl">              --&gt; 仅允许关注当前及之前 token
</span></span><span class="line"><span class="cl">        --&gt; 残差连接
</span></span><span class="line"><span class="cl">        --&gt; LayerNorm
</span></span><span class="line"><span class="cl">        --&gt; MLP（两个一维卷积核）
</span></span><span class="line"><span class="cl">        --&gt; 残差连接
</span></span><span class="line"><span class="cl">  --&gt; 线性映射到词表维度
</span></span><span class="line"><span class="cl">目标 token 序列
</span></span></code></pre></td></tr></table>
</div>
</div><h4 id="2预训练任务clm因果语言模型">(2)预训练任务&ndash;CLM(因果语言模型)</h4>
<p>Decoder-Only的结构更适合文本生成任务,所以其预训练任务也是最传统也最直接的预训练任务&ndash;因果语言模型,Causal Language Model，下简称 CLM
CLM 可以看作 N-gram 语言模型的一个直接扩展。N-gram 语言模型是基于前 N 个 token 来预测下一个 token，CLM 则是基于一个自然语言序列的前面所有 token 来预测下一个 token，通过<strong>不断重复该过程</strong>来实现目标文本序列的生成。也就是说，CLM 是一个经典的补全形式</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">input: 今天天气
</span></span><span class="line"><span class="cl">output: 今天天气很
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">input: 今天天气很
</span></span><span class="line"><span class="cl">output：今天天气很好
</span></span></code></pre></td></tr></table>
</div>
</div><p>BERT 之所以可以采用预训练+微调的范式取得重大突破，正是因为其选择的 MLM、NSP 可以在海量无监督语料上直接训练——而很明显，CLM 是更直接的预训练任务，其天生和人类书写自然语言文本的习惯相契合，也和下游任务直接匹配，相对于 MLM 任务更加直接，可以在任何自然语言文本上直接应用。因此，CLM 也可以使用海量的自然语言语料进行大规模的预训练</p>
<h4 id="3gpt系列模型的发展">(3)GPT系列模型的发展</h4>
<p>自 GPT-1 推出开始，OpenAI 一直坚信 Decoder-Only 的模型结构和“体量即正义”的优化思路，不断扩大预训练数据集、模型体量并对模型做出一些小的优化和修正，来不断探索更强大的预训练模型。从被 BERT 压制的 GPT-1，到没有引起足够关注的 GPT-2，再到激发了涌现能力、带来大模型时代的 GPT-3，最后带来了跨时代的 ChatGPT，OpenAI 通过数十年的努力证明了其思路的正确性。</p>
<p>下表总结了从 GPT-1 到 GPT-3 的模型结构、预训练语料大小的变化：</p>
<table>
	<thead>
			<tr>
					<th>模型</th>
					<th>Decoder Layer</th>
					<th>Hidden_size</th>
					<th>注意力头数</th>
					<th>注意力维度</th>
					<th>总参数量</th>
					<th>预训练语料</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>GPT-1</td>
					<td>12</td>
					<td>3072</td>
					<td>12</td>
					<td>768</td>
					<td>0.12B</td>
					<td>5GB</td>
			</tr>
			<tr>
					<td>GPT-2</td>
					<td>48</td>
					<td>6400</td>
					<td>25</td>
					<td>1600</td>
					<td>1.5B</td>
					<td>40GB</td>
			</tr>
			<tr>
					<td>GPT-3</td>
					<td>96</td>
					<td>49152</td>
					<td>96</td>
					<td>12288</td>
					<td>175B</td>
					<td>570GB</td>
			</tr>
	</tbody>
</table>
<p>GPT-1 是 GPT 系列的开山之作，也是第一个使用 Decoder-Only 的预训练模型。但是，GPT-1 的模型体量和预训练数据都较少，沿承了传统 Transformer 的模型结构，使用了 12层 Decoder Block 和 768 的隐藏层维度，模型参数量仅有 1.17亿（0.12B），在大小为 5GB 的 BooksCorpus 数据集上预训练得到。可以看到，GPT-1 的参数规模与预训练规模和 BERT-base 是大致相当的，但其表现相较于 BERT-base 却有所不如，这也是 GPT 系列模型没能成为预训练语言模型时代的代表的原因。</p>
<p>GPT-2 则是 OpenAI 在 GPT-1 的基础上进一步探究预训练语言模型多任务学习能力的产物。GPT-2 的模型结构和 GPT-1 大致相当，只是扩大了模型参数规模、将 Post-Norm 改为了 Pre-Norm（也就是先进行 LayerNorm 计算，再进入注意力层计算）。这些改动的核心原因在于，由于模型层数增加、体量增大，梯度消失和爆炸的风险也不断增加，为了使模型梯度更稳定对上述结构进行了优化。</p>
<p>GPT-2 的核心改进是大幅增加了预训练数据集和模型体量。GPT-2 的 Decoder Block 层数达到了48（注意，GPT-2 共发布了四种规格的模型，此处我们仅指规格最大的 GPT-2 模型），隐藏层维度达到了 1600，模型整体参数量达 15亿（1.5B），使用了自己抓取的 40GB 大小的 WebText 数据集进行预训练，不管是模型结构还是预训练大小都超过了 1代一个数量级。</p>
<p>GPT-2 的另一个重大突破是以 zero-shot（零样本学习）为主要目标，也就是不对模型进行微调，直接要求模型解决任务。例如，在传统的预训练-微调范式中，我们要解决一个问题，一般需要收集几百上千的训练样本，在这些训练样本上微调预训练语言模型来实现该问题的解决。而 zero-shot 则强调不使用任何训练样本，直接通过向预训练语言模型描述问题来去解决该问题。zero-shot 的思路自然是比预训练-微调范式更进一步、更高效的自然语言范式，但是在 GPT-2 的时代，模型能力还不足够支撑较好的 zero-shot 效果，在大模型时代，zero-shot 及其延伸出的 few-shot（少样本学习）才开始逐渐成为主流。</p>
<p>GPT-3 则是更进一步展示了 OpenAI“力大砖飞”的核心思路，也是 LLM 的开创之作。在 GPT-2 的基础上，OpenAI 进一步增大了模型体量和预训练数据量，整体参数量达 175B，是当之无愧的“大型语言模型”。在模型结构上，基本没有大的改进，只是由于巨大的模型体量使用了稀疏注意力机制来取代传统的注意力机制。在预训练数据上，则是分别从 CC、WebText、维基百科等大型语料集中采样，共采样了 45T、清洗后 570GB 的数据。根据推算，GPT-3 需要在 1024张 A100（80GB 显存）的分布式训练集群上训练 1个月。</p>
<p>之所以说 GPT-3 是 LLM 的开创之作，除去其巨大的体量带来了涌现能力的凸显外，还在于其提出了 few-shot 的重要思想。few-shot 是在 zero-shot 上的改进，研究者发现即使是 175B 大小的 GPT-3，想要在 zero-shot 上取得较好的表现仍然是一件较为困难的事情。而 few-shot 是对 zero-shot 的一个折中，旨在提供给模型少样的示例来教会它完成任务。few-shot 一般会在 prompt（也就是模型的输入）中增加 3~5个示例，来帮助模型理解。例如，对于情感分类任务：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">zero-shot：请你判断‘这真是一个绝佳的机会’的情感是正向还是负向，如果是正向，输出1；否则输出0
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">few-shot：请你判断‘这真是一个绝佳的机会’的情感是正向还是负向，如果是正向，输出1；否则输出0。你可以参考以下示例来判断：‘你的表现非常好’——1；‘太糟糕了’——0；‘真是一个好主意’——1。Copy to clipboardErrorCopied
</span></span></code></pre></td></tr></table>
</div>
</div><p>通过给模型提供少量示例，模型可以取得远好于 zero-shot 的良好表现。few-shot 也被称为上下文学习（In-context Learning），即让模型从提供的上下文中的示例里学习问题的解决方法。GPT-3 在 few-shot 上展现的强大能力，为 NLP 的突破带来了重要进展。如果对于绝大部分任务都可以通过人为构造 3~5个示例就能让模型解决，其效率将远高于传统的预训练-微调范式，意味着 NLP 的进一步落地应用成为可能——而这，也正是 LLM 的核心优势。</p>
<p>在 GPT 系列模型的基础上，通过引入预训练-指令微调-人类反馈强化学习的三阶段训练，OpenAI 发布了跨时代的 ChatGPT，引发了大模型的热潮。也正是在 GPT-3 及 ChatGPT 的基础上，LLaMA、ChatGLM 等模型的发布进一步揭示了 LLM 的无尽潜力。在下一节，我们将深入剖析目前 LLM 的普适架构——LLaMA。</p>
<p>总结要点:参数量变大;zero-shot;few-shot</p>
<h3 id="332-llama">3.3.2 LLaMA</h3>
<p>由Meta(前Facebook)开发</p>
<h4 id="1模型架构decoder-only-1">(1)模型架构&ndash;Decoder Only</h4>
<p><a href="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/3-figures/3-1.png">alt text</a>整体架构和GPT类似,只是在模型规模和预训练数据集有区别
位置编码部分也有区别:方式和时机
LLaMA:旋转编码(相对位置),在注意力机制中
GPT:正弦函数,余弦函数(绝对位置);在Embedding之后</p>
<p>与GPT类似，LLaMA模型的处理流程也始于将输入文本通过tokenizer进行编码，转化为一系列的input_ids。这些input_ids是模型能够理解和处理的数据格式。接下来，这些input_ids会经过embedding层的转换，这里每个input_id会被映射到一个高维空间中的向量，即词向量。同时，输入文本的位置信息也会通过positional embedding层被编码，以确保模型能够理解词序上下文信息。</p>
<p>这样，input_ids经过embedding层和positional embedding层的结合，形成了hidden_states。hidden_states包含了输入文本的语义和位置信息，是模型进行后续处理的基础，hidden_states随后被输入到模型的decoder层。</p>
<p>在decoder层中，hidden_states会经历一系列的处理，这些处理由多个decoder block组成。每个decoder block都是模型的核心组成部分，它们负责对hidden_states进行深入的分析和转换。在每个decoder block内部，首先是一个masked self-attention层。在这个层中，模型会分别计算query、key和value这三个向量。这些向量是通过hidden_states线性变换得到的，它们是计算注意力权重的基础。然后使用softmax函数计算attention score，这个分数反映了不同位置之间的关联强度。通过attention score，模型能够确定在生成当前词时，应该给予不同位置的hidden_states多大的关注。然后，模型将value向量与attention score相乘，得到加权后的value，这就是attention的结果。</p>
<p>在完成masked self-attention层之后，hidden_states会进入MLP层。在这个多层感知机层中，模型通过两个全连接层对hidden_states进行进一步的特征提取。第一个全连接层将hidden_states映射到一个中间维度，然后通过激活函数进行非线性变换，增加模型的非线性能力。第二个全连接层则将特征再次映射回原始的hidden_states维度。</p>
<p>最后，经过多个decoder block的处理，hidden_states会通过一个线性层进行最终的映射，这个线性层的输出维度与词表维度相同。这样，模型就可以根据hidden_states生成目标序列的概率分布，进而通过采样或贪婪解码等方法，生成最终的输出序列。这一过程体现了LLaMA模型强大的序列生成能力。</p>
<h4 id="2-发展历程">(2) 发展历程</h4>
<p><strong>LLaMA-1 系列</strong>：</p>
<ul>
<li>Meta于2023年2月发布了LLaMA-1，包括7B、13B、30B和65B四个参数量版本。</li>
<li>这些模型在超过1T token的语料上进行了预训练，其中最大的65B参数模型在2,048张A100 80G GPU上训练了近21天。</li>
<li>LLaMA-1因其开源性和优异性能迅速成为开源社区中最受欢迎的大模型之一。</li>
</ul>
<p><strong>LLaMA-2 系列</strong>：</p>
<ul>
<li>2023年7月，Meta发布了LLaMA-2，包含7B、13B、34B和70B四个参数量版本，除了34B模型外，其他均已开源。</li>
<li>LLaMA-2将预训练的语料扩充到了2T token，并将模型的上下文长度从2,048翻倍到了4,096。</li>
<li>引入了分组查询注意力机制（Grouped-Query Attention, GQA）等技术。</li>
</ul>
<p><strong>LLaMA-3 系列</strong>：</p>
<ul>
<li>2024年4月，Meta发布了LLaMA-3，包括8B和70B两个参数量版本，同时透露400B的LLaMA-3还在训练中。</li>
<li>LLaMA-3支持8K长文本，并采用了编码效率更高的tokenizer，词表大小为128K。</li>
<li>使用了超过15T token的预训练语料，是LLaMA-2的7倍多。</li>
</ul>
<p>LLaMA模型以其技术创新、多参数版本、大规模预训练和高效架构设计而著称。模型支持从70亿到数百亿不等的参数量，适应不同规模的应用需求。LLaMA-1以其开源性和优异性能迅速受到社区欢迎，而LLaMA-2和LLaMA-3进一步通过引入分组查询注意力机制和支持更长文本输入，显著提升了模型性能和应用范围。特别是LLaMA-3，通过采用128K词表大小的高效tokenizer和15T token的庞大训练数据，实现了在多语言和多任务处理上的重大进步。Meta对模型安全性和社区支持的持续关注，预示着LLaMA将继续作为AI技</p>
<h2 id="333-glm">3.3.3 GLM</h2>
<h4 id="1模型框架-相较于gpt的略微修正">(1)模型框架-相较于GPT的略微修正</h4>
<p>GLM 最初是由清华计算机系推出的一种通用语言模型基座，其核心思路是在传统 CLM 预训练任务基础上，加入 MLM 思想，从而构建一个在 NLG 和 NLU 任务上都具有良好表现的统一模型。</p>
<ol>
<li>使用 Post Norm 而非 Pre Norm。Post Norm 是指在进行残差连接计算时，先完成残差计算，再进行 LayerNorm 计算；而类似于 GPT、LLaMA 等模型都使用了 Pre Norm，也就是先进行 LayerNorm 计算，再进行残差的计算。相对而言，Post Norm 由于在残差之后做归一化，对参数正则化的效果更强，进而模型的鲁棒性也会更好；Pre Norm相对于因为有一部分参数直接加在了后面，不需要对这部分参数进行正则化，正好可以防止模型的梯度爆炸或者梯度消失。因此，对于更大体量的模型来说，一般认为 Pre Norm 效果会更好。但 GLM 论文提出，使用 Post Norm 可以避免 LLM 的数值错误（虽然主流 LLM 仍然使用了 Pre Norm）；</li>
<li>使用单个线性层实现最终 token 的预测，而不是使用 MLP；这样的结构更加简单也更加鲁棒，即减少了最终输出的参数量，将更大的参数量放在了模型本身；</li>
<li>激活函数从 ReLU 换成了 GeLUs。ReLU 是传统的激活函数，其核心计算逻辑为去除小于 0的传播，保留大于 0的传播；GeLUs 核心是对接近于 0的正向传播，做了一个非线性映射，保证了激活函数后的非线性输出，具有一定的连续性。</li>
</ol>
<h4 id="2预训练任务-glmgeneral-language-model通用语言模型">(2)预训练任务-GLM(General Language Model，通用语言模型)</h4>
<p>GLM 是一种结合了自编码思想和自回归思想的预训练方法:
所谓自编码思想，其实也就是 MLM 的任务学习思路，在输入文本中随机删除连续的 tokens，要求模型学习被删除的 tokens；所谓自回归思想，其实就是传统的 CLM 任务学习思路，也就是要求模型按顺序重建连续 tokens。</p>
<p>GLM 通过优化一个自回归空白填充任务来实现 MLM 与 CLM 思想的结合。其核心思想是，对于一个输入序列，会类似于 MLM 一样进行随机的掩码，但遮蔽的不是和 MLM 一样的单个 token，而是每次遮蔽一连串 token；模型在学习时，既需要使用遮蔽部分的上下文预测遮蔽部分，在遮蔽部分内部又需要以 CLM 的方式完成被遮蔽的 tokens 的预测。例如，输入和输出可能是：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">输入：I &lt;MASK&gt; because you &lt;MASK&gt;
</span></span><span class="line"><span class="cl">输出：&lt;MASK&gt; - love you; &lt;MASK&gt; - are a wonderful person
</span></span></code></pre></td></tr></table>
</div>
</div><p>不过，GLM 预训练任务更多的优势还是展现在预训练模型时代，迈入 LLM 时代后，针对于超大规模、体量的预训练，CLM 展现出远超 MLM 的优势。通过将模型体量加大、预训练规模扩大，CLM 预训练得到的生成模型在文本理解上也能具有超出 MLM 训练的理解模型的能力，因此，ChatGLM 系列模型也仅在第一代模型使用了 GLM 的预训练思想，从 ChatGLM2 开始，还是回归了传统的 CLM 建模。虽然从 LLM 的整体发展路径来看，GLM 预训练任务似乎是一个失败的尝试，但通过精巧的设计将 CLM 与 MLM 融合，并第一时间产出了中文开源的原生 LLM，其思路仍然存在较大的借鉴意义。</p>
<h4 id="3glm家族的发展">(3)GLM家族的发展:</h4>
<p>在 GLM 模型（即使用原生 GLM 架构及预训练任务的早期预训练模型）的基础上，参考 ChatGPT 的技术思路进行 SFT 和 RLHF，智谱于 23年 3月发布了第一个中文开源 LLM ChatGLM-6B，成为了众多中文 LLM 研究者的起点。ChatGLM-6B 在 1T 语料上进行预训练，支持 2K 的上下文长度。</p>
<p>在 23年 6月，智谱就开源了 ChatGLM2-6B。相对于一代，ChatGLM2 将上下文长度扩展到了 32K，通过更大的预训练规模实现了模型性能的大幅度突破。不过，在 ChatGLM2 中，模型架构就基本回归了 LLaMA 架构，引入 MQA 的注意力机制，预训练任务也回归经典的 CLM，放弃了 GLM 的失败尝试。</p>
<p>ChatGLM3-6B 发布于 23年 10月，相对于二代在语义、数学、推理、代码和知识方面都达到了当时的 SOTA 性能，但是官方给出的技术报告说明 ChatGLM3 在模型架构上相对二代没有变化，最主要的优化来源是更多样化的训练数据集、更充足的训练步骤和更优化的训练策略。ChatGLM3 的另一个重要改进在于其开始支持函数调用与代码解释器，开发者可以直接使用开源的 ChatGLM3 来实现 Agent 开发，具有更广泛的应用价值。</p>
<p>2024年 1月，智谱发布了支持 128K 上下文，包括多种类型的 GLM-4 系列模型，评估其在英文基准上达到了 GPT-4 的水平。不过，智谱并未直接开源 GLM-4，而是开源了其轻量级版本 GLM-4-9B 模型，其在 1T token 的多语言语料库上进行预训练，上下文长度为 8K，并使用与 GLM-4 相同的管道和数据进行后训练。在训练计算量较少的情况下，其超越了 Llama-3-8B，并支持 GLM-4 中所有工具的功能。</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
