<?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>LLM on On My Way</title>
    <link>https://tuanzi-wow.cn/categories/llm/</link>
    <description>Recent content in LLM on On My Way</description>
    <generator>Hugo</generator>
    <language>zh-CN</language>
    <lastBuildDate>Sat, 12 Sep 2026 00:19:57 +0800</lastBuildDate>
    <atom:link href="https://tuanzi-wow.cn/categories/llm/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>NLP 基础概念</title>
      <link>https://tuanzi-wow.cn/llm/llm-nlp-basics/</link>
      <pubDate>Sun, 06 Sep 2026 00:00:00 +0800</pubDate>
      <guid>https://tuanzi-wow.cn/llm/llm-nlp-basics/</guid>
      <description>NLP 基础：规则方法到统计学习再到深度学习，文本表示与词向量演进。</description>
      <content:encoded><![CDATA[<h1 id="第一章nlp基础概念">第一章：NLP基础概念</h1>
<p>NPL: Natural Language Processing(自然语言处理)
使计算机能够理解和处理人类语言,实现人机交流
从早期的基于规则的方法,到后来的统计学习方法,再到当前深度学习技术的广泛应用
文本表示作为NPL的核心技术之一,对于NPL系统的性能提升具有决定性的作用</p>
<h2 id="11-什么是nlp">1.1 什么是NLP</h2>
<p>让计算机理解,解释和生成人类语言的技术.通过计算机程序来模拟热泪对于语言的认知和使用过程,使一门跨多种学科的技术</p>
<p>NLP技术使得计算机能够执行各种复杂的语言处理任务,这些任务不仅要求计算机能够识别和处理语言的表层结构,更重要的是理解语言背后的深层含义,包括语义、语境、情感和文化等方面的复杂因素.</p>
<p>进入深度学习阶段,NLP获得了显著的进步目的,但是NLP仍然面临着大量的挑战没比如:如处理歧义性、理解抽象概念、处理隐喻和讽刺等</p>
<h2 id="12-nlp发展历程">1.2 NLP发展历程</h2>
<p>规则基础方法-&gt;符号注意和统计方法-&gt;机器学习和深度学习</p>
<h3 id="早期探索">早期探索</h3>
<p>图灵提出:
他说，如果一台机器可以通过使用打字机成为对话的一部分，并且能够完全模仿人类，没有明显的差异，那么机器可以被认为是能够思考的。</p>
<p>这是判断机器是否能够展现出与人类不可区分的智能行为的测试
同时,诺姆·乔姆斯基提出了生成语法理论
这一时期的机器翻译系统非常简单，主要依赖字典查找和基本的词序规则来进行翻译</p>
<h3 id="符号主义与统计探索">符号主义与统计探索</h3>
<p>主要分为:符号主义和统计方法两大阵营
符号主义关注形式语言和生成语法
统计方法关注统计与概率方法
1980年代,统计模型开始取代复杂的&quot;手写&quot;规则</p>
<h3 id="机器学习和深度学习">机器学习和深度学习</h3>
<p>深度学习模型如循环神经网络（Recurrent Neural Network，RNN）、长短时记忆网络（Long Short-Term Memory，LSTM）和注意力机制等技术被广泛应用于 NLP 任务中
Word2Vec模型的提出开创了词向量表示的新时代
2018年.BERT模型引领了预训练语言模型的新浪潮
近年来,基于Transformer</p>
<h2 id="13-nlp任务">1.3 NLP任务</h2>
<p>涵盖了从文本的基本处理到复杂的语义理解和生成的各个方面
核心任务:包括但不限于中文分词、子词切分、词性标注、文本分类、实体识别、关系抽取、文本摘要、机器翻译以及自动问答系统的开发</p>
<h3 id="131-中文分词chinese-word-segmentation-cws">1.3.1 中文分词(Chinese Word Segmentation, CWS)</h3>
<p>中文词与词之间没有明确的分割
中文分词成为了中午文本处理的首要步骤,目的是将连续的中文文本切分成有意义的词汇序列</p>
<p>正确的分词结果对于后续的词性标注、实体识别、句法分析等任务至关重要。如果分词不准确，将直接影响到整个文本处理流程的效果。</p>
<h3 id="132-字词区分subword-segmentation">1.3.2 字词区分(Subword Segmentation)</h3>
<p>子词切分（Subword Segmentation）是 NLP 领域中的一种常见的文本预处理技术，旨在将词汇进一步分解为更小的单位，即子词。</p>
<p>子词切分特别适用于处理词汇稀疏问题，即当遇到罕见词或未见过的新词时，能够通过已知的子词单位来理解或生成这些词汇。子词切分在处理那些拼写复杂、合成词多的语言（如德语）或者在预训练语言模型（如BERT、GPT系列）中尤为重要,例如:</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">输入：unhappiness
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">不使用子词切分：整个单词作为一个单位，输出：“unhappiness”
</span></span><span class="line"><span class="cl">使用子词切分（假设BPE算法）：单词被分割为：“un”、“happi”、“ness”Copy to clipboardErrorCopied
</span></span></code></pre></td></tr></table>
</div>
</div><p>在这个例子中，通过子词切分，“unhappiness”这个词被分解成了三个部分：前缀“un”表示否定，“happi”是“happy”的词根变体，表示幸福，“ness”是名词后缀，表示状态。即使模型从未见过“unhappiness”这个完整的单词，它也可以通过这些已知的子词来理解其大致意思为“不幸福的状态”。</p>
<p>子词切分的方法有很多种，常见的有Byte Pair Encoding (BPE)、WordPiece、Unigram、SentencePiece等。
这些方法的基本思想是将单词分解成更小的、频繁出现的片段，这些片段可以是单个字符、字符组合或者词根和词缀。</p>
<h3 id="133-词性标注part-of-speech-taggingpos-tagging">1.3.3 词性标注(Part-of-Speech Tagging，POS Tagging)</h3>
<p>它的目标是为文本中的每个单词分配一个词性标签，如名词、动词、形容词等</p>
<p>词性标注通常依赖于机器学习模型，如隐马尔可夫模型（Hidden Markov Model，HMM）、条件随机场（Conditional Random Field，CRF）或者基于深度学习的循环神经网络 RNN 和长短时记忆网络 LSTM 等。这些模型通过学习大量的标注数据来预测新句子中每个单词的词性。</p>
<h3 id="134-文本分类">1.3.4 文本分类</h3>
<p>到将给定的文本自动分配到一个或多个预定义的类别中。这项技术广泛应用于各种场景
关键:文本分类的关键在于理解文本的含义和上下文，并基于此将文本映射到特定的类别
文本分类任务的成功关键在于选择合适的特征表示和分类算法，以及拥有高质量的训练数据
随着深度学习技术的发展，使用神经网络进行文本分类已经成为一种趋势，它们能够捕捉到文本数据中的复杂模式和语义信息</p>
<h3 id="135-实体识别named-entity-recognition-ner">1.3.5 实体识别(Named Entity Recognition, NER)</h3>
<p>也称为命名实体类别
旨在自动识别文本中具有特定意义的实体，并将它们分类为预定义的类别</p>
<h3 id="136-关系抽取relation-extraction">1.3.6 关系抽取(Relation Extraction)</h3>
<p>它的目标是从文本中识别实体之间的语义关系。这些关系可以是因果关系、拥有关系、亲属关系、地理位置关系等</p>
<h3 id="137-文本摘要text-summarization">1.3.7 文本摘要(Text Summarization)</h3>
<p>目的是生成一段简洁准确的摘要，来概括原文的主要内容
根据生成方式的不同,可以分为两大类:
抽取式摘要(Extractive Summarization) :从原文中选取关键句子和短语来组成摘要
优点:信息来自原文,准确
缺点:拼接,不够流畅</p>
<p>生成式摘要(Abstractive Summarization):
选择文本片段,重新组织和改写
更具有挑战性,需要理解文本深层含义并且表达相同的信息</p>
<h3 id="138-机器翻译machine-translation-mt">1.3.8 机器翻译(Machine Translation, MT)</h3>
<p>指使用计算机程序将一种自然语言（源语言）自动翻译成另一种自然语言（目标语言）的过程</p>
<p>机器翻译不仅涉及到词汇的直接转换，更重要的是要准确传达源语言文本的语义、风格和文化背景等</p>
<p>为了提高机器翻译的质量，研究者不断探索新的方法和技术，如基于神经网络的Seq2Seq模型、Transformer模型等，这些模型能够学习到源语言和目标语言之间的复杂映射关系，从而实现更加准确和流畅的翻译</p>
<h3 id="139-自动问答automatic-question-answering-qa">1.3.9 自动问答(Automatic Question Answering, QA)</h3>
<p>旨在使计算机能够理解自然语言提出的问题，并根据给定的数据源自动提供准确的答案
自动问答系统的构建涉及多个NLP子任务，如信息检索、文本理解、知识表示和推理等</p>
<p>自动问答可分为三类:
检索式问答(Retrieval-based QA):通过搜索引擎等方式从大量文本中检索答案
知识库问答(Knowledge-based QA):通过结构化的知识库回答
社区问答(Community-based QA):依赖于用户生成的问答数据</p>
<h2 id="14-文本表示的发展历程">1.4 文本表示的发展历程</h2>
<p>文本表示的目的是将人类语言的自然形式转化为计算机可以处理的形式，也就是将文本数据数字化，使计算机能够对文本进行有效的分析和处理。文本表示是 NLP 领域中的一项基础性和必要性工作，它直接影响甚至决定着 NLP 系统的质量和性能。</p>
<p>在 NLP 中，文本表示涉及到将文本中的语言单位（如字、词、短语、句子等）以及它们之间的关系和结构信息转换为计算机能够理解和操作的形式，例如向量、矩阵或其他数据结构。这样的表示不仅需要保留足够的语义信息，以便于后续的 NLP 任务，如文本分类、情感分析、机器翻译等，还需要考虑计算效率和存储效率。</p>
<p>文本表示的发展历程经历了多个阶段，从早期的基于规则的方法，到统计学习方法，再到当前的深度学习技术，文本表示技术不断演进，为 NLP 的发展提供了强大的支持。</p>
<h3 id="141-词向量">1.4.1 词向量</h3>
<p><strong>向量空间模型（Vector Space Model, VSM）:</strong>
是 NLP 领域中一个基础且强大的文本表示方法，最早由哈佛大学Salton提出。向量空间模型通过将文本（包括单词、句子、段落或整个文档）转换为高维空间中的向量来实现文本的数学化表示。在这个模型中，每个维度代表一个特征项（例如，字、词、词组或短语），而向量中的每个元素值代表该特征项在文本中的权重，这种权重通过特定的计算公式（如词频TF、逆文档频率TF-IDF等）来确定，反映了特征项在文本中的重要程度。</p>
<p><strong>应用:</strong>
包括但不限于文本相似度计算、文本分类、信息检索等自然语言处理任务</p>
<p><strong>问题:</strong>
1.最主要的是数据稀疏性和维数灾难问题:特征项数量庞大导致向量维度极高，同时多数元素值为零
2.此外，由于模型基于特征项之间的独立性假设，忽略了文本中的结构信息，如词序和上下文信息，限制了模型的表现力。
3.特征项的选择和权重计算方法的不足也是向量空间模型需要解决的问题</p>
<p><strong>词汇表:</strong>
词汇表是一个包含所有可能出现的词语的集合。在向量空间模型中，每个词对应词汇表中的一个位置，通过这种方式可以将词语转换为向量表示</p>
<p><strong>改进:</strong>
1.改进特征表示方法:如借助图方式,主题方式等进行关键词提取
2.改进和优化特征项权重的计算方式</p>
<h3 id="142-语言模型">1.4.2 语言模型</h3>
<p>N-gram模型:基于统计的语言模型
核心思想:基于马尔可夫假设,即一个词的出现概率仅依赖于它前面的N-1个词
例如:
当N=1时，模型称为unigram，仅考虑单个词的概率；当N=2时，称为bigram，考虑前一个词来估计当前词的概率；当N=3时，称为trigram，考虑前两个词来估计第三个词的概率，以此类推N-gram</p>
<p>N-gram模型通过条件概率链式规则来估计整个句子的概率
对于给定的一个句子，模型会计算每个N-gram出现的条件概率，并将这些概率相乘以得到整个句子的概率,
例如:
对于句子“The quick brown fox”，作为trigram模型，我们会计算 P(&ldquo;brown&rdquo;∣&ldquo;The&rdquo;,&ldquo;quick&rdquo;)<em>P</em>(&quot;<em>b<strong>r</strong>o<strong>w</strong>n</em>&quot;∣&quot;<em>T<strong>h</strong>e</em>&quot;,&quot;<em>q<strong>u</strong>i<strong>c</strong>k</em>&quot;)、P(&ldquo;fox&rdquo;∣&ldquo;quick&rdquo;,&ldquo;brown&rdquo;)<em>P</em>(&quot;<em>f<strong>o</strong>x</em>&quot;∣&quot;<em>q<strong>u</strong>i<strong>c</strong>k</em>&quot;,&quot;<em>b<strong>r</strong>o<strong>w</strong>n</em>&quot;)等概率，并将它们相乘</p>
<p>优点:实现简单,容易理解</p>
<p>缺点:N过大出现数据稀疏性问题;参数空间过大,相同的</p>
<h3 id="143-word2vec">1.4.3 Word2Vec</h3>
<p>通过学习词的上下文关系来生成词的密集向量表示
核心思想:利用词在文本中的上下文信息来捕捉词之间的语义关系,从而使得语义相似或相关的词在向量空间中距离较近
主要有两种架构:
Word2Vec模型主要有两种架构：连续词袋模型CBOW(Continuous Bag of Words)是根据目标词上下文中的词对应的词向量, 计算并输出目标词的向量表示；Skip-Gram模型与CBOW模型相反, 是利用目标词的向量表示计算上下文中的词向量. 实践验证CBOW适用于小型数据集, 而Skip-Gram在大型语料中表现更好</p>
<p>优点:向量密集型,减少计算复杂度和存储需求;捕捉词之间的语义关系,可以更好地泛化没见过地词
缺点:基于局部上下文,无法捕捉到长距离的依赖关系</p>
<h3 id="144-elmoembeddings-from-language-models">1.4.4 ElMo(Embeddings from Language Models)</h3>
<p>实现了一词多义,静态词向量到动态词向量的跨越式转变</p>
<p>分为两个阶段:
第1个阶段是利用语言模型进行预训练;
第2个阶段是在做特定任务时, 从预训练网络中提取对应单词的词向量作为新特征补充到下游任务中</p>
<p>优点:能够捕捉词汇的多义性和上下文信息,生成的词向量也更加丰富准确
缺点:模型复杂度高,训练时间长,计算资源消耗大等等</p>
]]></content:encoded>
    </item>
    <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>
    <item>
      <title>LLM · 大语言模型</title>
      <link>https://tuanzi-wow.cn/llm/llm-large-language-model/</link>
      <pubDate>Sun, 06 Sep 2026 00:00:00 +0800</pubDate>
      <guid>https://tuanzi-wow.cn/llm/llm-large-language-model/</guid>
      <description>LLM 的定义与涌现能力、Pretrain/SFT/RLHF 三阶段训练、ZeRO 与分布式训练、RLHF 与 DPO。</description>
      <content:encoded><![CDATA[<h1 id="第四章-大语言模型">第四章 大语言模型</h1>
<h2 id="41-什么是llm">4.1 什么是LLM</h2>
<h3 id="441-llm的定义">4.4.1 LLM的定义</h3>
<p>一般来说,拥有更庞大的参数,在更海量的语料上进行预训练
广义的 LLM 一般覆盖了从<strong>十亿参数</strong>（如 Qwen-1.5B）到<strong>千亿参数</strong>（如 Grok-314B）的所有大型语言模型。只要模型展现出<strong>涌现能力</strong>，即在一系列复杂任务上表现出远超传统预训练模型（如 BERT、T5）的能力与潜力，都可以称之为 LLM。</p>
<p>一般认为,GPT-3是LLM的开端,基于GPT-3通过预训练(Pretraining),监督微调(Supervised Fine-Tuning，SFT),强化学习和人类反馈(Reinforcement Learning with Human Feedback，RLHF)三阶段训练得到的ChatGPT更是主宰了LLM时代的到来</p>
<h3 id="412-llm的能力">4.1.2 LLM的能力</h3>
<h4 id="1涌现能力emergent-abilities">(1)涌现能力(Emergent Abilities)</h4>
<p>区别LLM于传统的PLM最显著的特征就是LLM具备涌现能力:
指的是,在相同的架构和预训练任务下,某些能力在小型模型中不明显,但是在大型模型下非常突出(量变引起质变)</p>
<h4 id="2上下文学习in-context-learning">(2)上下文学习(In-context Learning)</h4>
<p>有GPT-3首次引入,具体而言,上下文学习指的是:
允许语言模型在提供自然语言指令或多个任务示例的情况下，通过理解上下文并生成相应输出的方式来执行任务，而无需额外的训练或参数更新</p>
<p>而具备上下文学习能力的 LLM 往往无需进行高成本的额外训练或微调，而可以通过少数示例或是调整自然语言指令，来处理绝大部分任务，从而大大节省了算力和数据成本</p>
<p>对于具备上下文学习的LLM来说,一般范式已经开始向Prompt Engineering(调整Prompt)来激发LLM的能力转变</p>
<h4 id="3指令遵循instruction-following">(3)指令遵循(Instruction Following)</h4>
<p>指令微调的情况下,也就是使用自然语言描述的多任务数据进行微调
经过指令微调的LLM可以理解并遵循未见过的指令,具有强大的泛化能力</p>
<h4 id="4逐步推理step-by-step-reasoning">(4)逐步推理(Step by Step Reasoning)</h4>
<p>LLM可以通过思维链(Chain of thought,CoT)推理策略,利用包含中间推理步骤的提示机制来解决复杂的逻辑推理任务</p>
<h3 id="413-llm的特点">4.1.3 LLM的特点</h3>
<h4 id="1多语言支持">(1)多语言支持</h4>
<p>训练的语料本事就是多语言的,但是目前英语的高质量语料仍是占据大多数</p>
<h4 id="2长文本支持">(2)长文本支持</h4>
<p>一:旋转位置编码（Rotary Positional Encoding，RoPE）,具有一定的长度外推能力
二:支持长文本</p>
<h4 id="3拓展多模态">(3)拓展多模态</h4>
<p>图片等等</p>
<h4 id="4幻觉">(4)幻觉</h4>
<p>LLM根据Prompt杜撰生成虚假,错误信息的表现
削弱幻觉的方式:Prompt中显式限制,通过RAG来指导生成,但是无法根除</p>
<h2 id="42-如何训练一个llm">4.2 如何训练一个LLM</h2>
<p>训练LLM的三个阶段
<a href="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/4-figures/2-0.jpg">alt text</a></p>
<p>训练一个完整的LLM需要三个阶段&ndash;Pretrain,SFT和RLHF</p>
<h3 id="421-pretrain">4.2.1 Pretrain</h3>
<p>预训练:最核心也是工程量最大的一步
海量无监督文本对随机初始化的模型参数进行训练
由于主流的LLM都采用了Decoder-Only的类GPT框架(LLaMA框架),他们的训练任务为经典预训练任务&ndash;因果语言模型(CLM)</p>
<p>LLM预训练于传统模型的预训练的最核心差异在于:预训练的体量和资源消耗
更大的参数量和更海量的语料
训练语料的量:
1.根据由 OpenAI 提出的 Scaling Law：C ~ 6ND，其中 C 为计算量，N 为模型参数，D 为训练的 token 数，可以实验得出训练 token 数应该是模型参数的 1.7倍
2.而 LLaMA 更是进一步提出，使用 20倍 token 来训练模型能达到效果最优</p>
<p>分布式训练框架是LLM训练必不可少的组成部分
核心思路:数据并行和模型并行
数据并行:训练模型的尺寸可以被单个 GPU 内存容纳
但是:由于增大训练的 batch_size 会增大显存开销，无法使用较大的 batch_size 进行训练;训练数据量非常大，使用单张 GPU 训练时长难以接受</p>
<p>但是，当 LLM 扩大到上百亿参数，单张 GPU 内存往往就无法存放完整的模型参数。如图4.3所示，在这种情况下，可以将模型拆分到多个 GPU 上，每个 GPU 上存放不同的层或不同的部分，从而实现模型并行</p>
<p>在此思想基础上,还演化出了其他的方式,例如:
张量并行、3D 并行、ZeRO（Zero Redundancy Optimizer，零冗余优化器)
主流的分布式训练框架包括:Deepspeed、Megatron-LM、ColossalAI 等，其中，Deepspeed 使用面最广</p>
<p>DeepSpeed的核心策略是ZeRO和CPU-offload
ZeRO是一种显存优化的数据并行方案,其核心思想是:优化数据并行的每张卡的显存占用,从而实现对更大规模模型的支持
它将模型训练阶段每张卡被占用的显存分为两类:</p>
<ul>
<li>模型状态（Model States），包括模型参数、模型梯度和优化器 Adam 的状态参数。假设模型参数量为 1M，一般来说，在混合精度训练的情况下，该部分需要 16M 的空间进行存储，其中 Adam 状态参数会占据 12M 的存储空间。</li>
<li>剩余状态（Residual States），除了模型状态之外的显存占用，包括激活值、各种缓存和显存碎片</li>
</ul>
<p>针对上述显存占用，ZeRO 提出了三种不断递进的优化策略：</p>
<ol>
<li>ZeRO-1，对模型状态中的 Adam 状态参数进行分片，即每张卡只存储 1N<em>N</em>1 的 Adam 状态参数，其他参数仍然保持每张卡一份。</li>
<li>ZeRO-2，继续对模型梯度进行分片，每张卡只存储 1N<em>N</em>1 的模型梯度和 Adam 状态参数，仅模型参数保持每张卡一份。</li>
<li>ZeRO-3，将模型参数也进行分片，每张卡只存储 1N<em>N</em>1 的模型梯度、模型参数和 Adam 状态参数。</li>
</ol>
<p>随着分片的参数量不断增加，每张卡需要占用的显存也不断减少。当然，分片的增加也就意味着训练中通信开销的增加,每张卡的 GPU 利用率 ZeRO-1 最高而 ZeRO-3 最低</p>
<p>另一大挑战:训练数据本身:LLM的大部分知识都是在预训练中获得的,因此预训练的语料应该组织多种来源的数据并且以一定比例进行混合
事实上，数据配比向来是预训练 LLM 的“核心秘籍”，不同的配比往往会相当大程度影响最终模型训练出来的性能
问题:目前高质量的语料大部分还是集中在英文范畴</p>
<p>预训练数据的处理与清洗也是一个重要环节:预训练数据的质量往往比体量更重要,</p>
<p>预训练数据处理一般包含以下流程:</p>
<ol>
<li>文档准备。由于海量预训练语料往往是从互联网上获得，一般需要从爬取的网站来获得自然语言文档。文档准备主要包括 URL 过滤（根据网页 URL 过滤掉有害内容）、文档提取（从 HTML 中提取纯文本）、语言选择（确定提取的文本的语种）等。</li>
<li>语料过滤。语料过滤的核心目的是去除低质量、无意义、有毒有害的内容，例如乱码、广告等。语料过滤一般有两种方法：基于模型的方法，即通过高质量语料库训练一个文本分类器进行过滤；基于启发式的方法，一般通过人工定义 web 内容的质量指标，计算语料的指标值来进行过滤。</li>
<li>语料去重。实验表示，大量重复文本会显著影响模型的泛化能力，因此，语料去重即删除训练语料中相似度非常高的文档，也是必不可少的一个步骤。去重一般基于 hash 算法计算数据集内部或跨数据集的文档相似性，将相似性大于指定阈值的文档去除；也可以基于子串在序列级进行精确匹配去重。</li>
</ol>
<p>目前有很多经过处理的高质量预训练语料专用于预训练数据处理的框架</p>
<p>有基于 LLaMA 思路收集、清洗的预训练数据集<a href="https://huggingface.co/datasets/togethercomputer/RedPajama-Data-1T">RedPajama-1T</a>，以及在 RedPajama 基础上进行筛选去重的<a href="https://huggingface.co/datasets/cerebras/SlimPajama-627B/tree/main/train">SlimPajama-627B</a>数据集，实验证明高质量的 627B Slimpajama 数据集能够获得比 1T 的 RedPajama 数据集更好的效果。
(客观论证了预训练数据的质量比体量更重要)</p>
<h3 id="422-sft有监督微调">4.2.2 SFT(有监督微调)</h3>
<p>通过选择训练模型的&quot;通用指令遵循能力&quot;,也就是一般通过<strong>指令微调</strong>的方式来进行SFT</p>
<p>SFT的主要目标就是让模型从多种类型,多种风格的指令中获得泛化的指令遵循能力,也就是能够理解和回复用户的指令
因此:SFT的数据质量和数据配比也是决定模型指令遵循能力的重要因素</p>
<p>首先是指令数据量以及覆盖范围:
需要收集类别各异的用户指令和对应回复对LLM进行训练,为了使得泛化的指令遵循能力,需要在训练数据集中覆盖多种类型的任务指令,也需要较大的数据量</p>
<p>不同类型的指令数据中间的配比也是LLM训练的一大挑战:</p>
<p>高质量的指令数据集具有较高的获取难度。不同于预训练使用的无监督语料，SFT 使用的指令数据集是有监督语料，除去设计广泛、合理的指令外，还需要对指令回复进行人工标注，并保证标注的高质量</p>
<p>为降低数据成本，部分学者提出了使用 ChatGPT 或 GPT-4 来生成指令数据集的方法。例如，经典的开源指令数据集 <a href="https://github.com/yizhongw/self-instruct/blob/main/human_eval/user_oriented_instructions.jsonl">Alpaca</a>就是基于一些种子 Prompt，通过 ChatGPT 生成更多的指令并对指令进行回复来构建的。</p>
<p>一般 SFT 所使用的指令数据集包括以下三个键：</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-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;instruction&#34;</span><span class="p">:</span><span class="s2">&#34;即输入的用户指令&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;input&#34;</span><span class="p">:</span><span class="s2">&#34;执行该指令可能需要的补充输入，没有则置空&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;output&#34;</span><span class="p">:</span><span class="s2">&#34;即模型应该给出的回复&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>同时，为使模型能够学习到和预训练不同的范式，在 SFT 的过程中，往往会针对性设置特定格式。例如，LLaMA 的 SFT 格式为：</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">### Instruction:\n{{content}}\n\n### Response:\n
</span></span></code></pre></td></tr></table>
</div>
</div><p>其中的 content 即为具体的用户指令，也就是说，对于每一个用户指令，将会嵌入到上文的 content 部分，这里的用户指令不仅指上例中的 “instruction”，而是指令和输入的拼接，即模型可以执行的一条完整指令。例如，针对上例，LLaMA 获得的输入应该是：</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">### Instruction:\n将下列文本翻译成英文：今天天气真好\n\n### Response:\n
</span></span></code></pre></td></tr></table>
</div>
</div><p>注意，因为指令微调本质上仍然是对模型进行 CLM 训练，只不过要求模型对指令进行理解和回复而不是简单地预测下一个 token，所以模型预测的结果不仅是 output，而应该是 input + output，只不过 input 部分不参与 loss 的计算，但回复指令本身还是以预测下一个 token 的形式来实现的。</p>
<p>模型是否支持多轮对话，与预训练是没有关系的。事实上，模型的多轮对话能力完全来自于 SFT 阶段。如果要使模型支持多轮对话，我们需要在 SFT 时将训练数据构造成多轮对话格式，让模型能够利用之前的知识来生成回答</p>
<p>构造多轮对话样本一般有三种方式:
1.直接将最后一次模型回复作为输出,前面所有历史会话作为输入,直接拟合最后一次回复
2.将N轮多花构造成N个样本
3.直接要求模型预测每一轮对话的输出</p>
<p>第一种方式会丢失大量中间信息，第二种方式造成了大量重复计算，只有第三种方式是最合理的多轮对话构造。</p>
<p>我们之所以可以以第三种方式来构造多轮对话样本，是因为 LLM 本质还是进行的 CLM 任务，进行单向注意力计算，因此在预测时会从左到右依次进行拟合，前轮的输出预测不会影响后轮的预测。目前，绝大部分 LLM 均使用了多轮对话的形式来进行 SFT</p>
<h3 id="423-rlhf人类反馈强化学习">4.2.3 RLHF(人类反馈强化学习)</h3>
<p>利用强化学习来训练LLM的关键步骤
RLHF往往被认为ChatGPT相较于GPT3最核心的突破
从功能上出发,我们可以将LLM的训练过程分为预训练与对齐两个阶段</p>
<p>预训练的核心功能是赋予模型海量知识;二对齐,则是让模型与人类价值观一致,从而输出人类希望输出的内容
SFT是让LLM和人类指令对齐,从而拥有指令遵循的能力;而RLHF则是从更深层次使LLM与人类价值观对齐,从而达到安全有用无害的和核心标准</p>
<p>ChatGPT在技术报告中的对齐分为三个阶段:后面两个阶段训练RM和PPO训练,就是RLHF的步骤
<a href="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/4-figures/2-3.png">alt text</a></p>
<p>RLHF的思路:引入强化学习的技术,通过实时的人类反馈零LLM能够给出更令人满意的回复
强化学习是有别于监督学习的另一种机器学习的方法,主要由两个部分构成:智能体和环境</p>
<p>pretrain传授基础知识;SFT教他怎么做题;RLHF类似于真正的训练</p>
<p>RLHF分为两个步骤:训练RM和PPO训练
RM: Reward Model 即奖励模型
本质上是一个文本分类模型</p>
<p>RM，Reward Model，即奖励模型。RM 是用于拟合人类偏好，来给 LLM 做出反馈的。在强化学习的训练中，对于 LLM 的每一个回复，RM 会进行打分，这个打分反映了生成回复符合人类偏好的程度。然后 LLM 会根据强化学习的原理，基于 RM 的打分来进行优化训练。所以，RM 本质上是一个文本分类模型，对于一个文本输出一个标量奖励，和文本分类任务中的隐藏层输出非常类似。在具体实现上，RM 也往往就是传统的 LLM 架构（或 BERT 架构）加上一层分类层，和用于文本分类的 LLM 架构完全一致，只不过使用隐藏层输出而不是最后的分类输出而已。</p>
<p>但是，在训练 RM 时，我们往往并不直接使用文本及其对应的标量奖励来对 RM 进行训练。因为要对齐人类偏好，RM 训练的偏好数据往往是由人工标注的。但是，由于标注者之间往往也存在价值观差异，数值形式的标量奖励往往会将这些差异放大，从而导致在训练过程中对同样程度的回复奖励不一致，模型难以拟合到正确的标量奖励。因此，我们往往对同一个 completion 下的不同回复进行排名，再将排名转化为奖励。</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></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;prompt&#34;</span><span class="p">:</span><span class="s2">&#34;如果你打算从商店偷东西，你觉得早上好还是晚上好？&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;chosen&#34;</span><span class="p">:</span><span class="s2">&#34;这是违法的事情，我不能提供建议&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;rejected&#34;</span><span class="p">:</span><span class="s2">&#34;考虑晚上的人口贩运和监控摄像头的差别是件好事。夜间时间可能更有利于避免监控摄像头,但晚上的商店雇员会更能看见你。另一方面,由于白天通常不太忙,因此更容易避免被其他人注意到。无论如何,必须密切注意商店雇员和所有顾客的行为。他们也许能够看见你,即使他们不是直接面对你。为了安全起见,重要的是要密切注意商店里的一切事情,而不是不小心。&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span><span class="err">Copy</span> <span class="err">to</span> <span class="err">clipboardErrorCopied</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>其中，prompt 是用户的问题，chosen 是应该对齐的、符合人类偏好的回答，rejected 是不符合人类偏好的回答。在训练中，prompt 将和 chosen 以及 rejected 分别拼接起来，形成 chosen_example 和 rejected_example，然后分别进入模型通过前向传播输出一个标量奖励。然后模型会通过最大化 chosen_example 和 rejected_example 的标量差异来计算 loss，并进行反向传播完成训练。</p>
<p>值得注意的是，RM 训练使用的模型往往和最后的 LLM 大小不同。例如 OpenAI 使用了 175B 的 LLM 和 6B 的 RM。同时，RM 使用的模型可以是经过 SFT 之后的 LM，也可以是基于偏好数据从头训练的 RM。哪一种更好，至今尚没有定论。</p>
<p>在完成 RM 训练之后，就可以使用 PPO 算法来进行强化学习训练。PPO，Proximal Policy Optimization，近端策略优化算法，是一种经典的 RL 算法。事实上，强化学习训练时也可以使用其他的强化学习算法，但目前 PPO 算法因为成熟、成本较低，还是最适合 RLHF 的算法。</p>
<p>在具体 PPO 训练过程中，会存在四个模型。如图4.5所示，两个 LLM 和两个 RM。两个 LLM 分别是进行微调、参数更新的 actor model 和不进行参数更新的 ref model，均是从 SFT 之后的 LLM 初始化的。两个 RM 分别是进行参数更新的 critic model 和不进行参数更新的 reward model，均是从上一步训练的 RM 初始化的。</p>
<p><a href="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/4-figures/2-4.jpg">alt text</a></p>
<p>图4.5 PPO 训练流程</p>
<p>如上图，使用 PPO 算法的强化学习训练过程如下：</p>
<ol>
<li>从 SFT 之后的 LLM 初始化两个模型分别作为 Actor Model 和 Ref Model；从训练的 RM 初始化两个模型分别作为 Reward Model 和 Critic Model；</li>
<li>输入一个 Prompt，Actor Model 和 Ref Model 分别就 Prompt 生成回复；</li>
<li>Actor Response 和 Ref Response 计算 KL 散度： rKL=−θKLDKL(πPPO(y∣x)∣∣πbase(y∣x))<em>r<strong>K</strong>L</em>=−<em>θ<strong>K</strong>L<strong>D</strong>K**L</em>(<em>π<strong>P</strong>P**O</em>(<em>y</em>∣<em>x</em>)∣∣<em>π<strong>ba</strong>se</em>(<em>y</em>∣<em>x</em>)) 其中， πPPO(y∣x)<em>π<strong>P</strong>P**O</em>(<em>y</em>∣<em>x</em>) 即为 Actor Model 的输出，而 πbase(y∣x)<em>π<strong>ba</strong>se</em>(<em>y</em>∣<em>x</em>) 即为 Ref Model 的输出， θKLDKL<em>θ<strong>K</strong>L<strong>D</strong>K**L</em> 即是计算 KL 散度的方法；</li>
<li>Actor Response 分别输入到 Reward Model 和 Critic Model 进行打分，其中，Reward Model 输出的是回复对应的标量奖励，Critic Model 还会输出累加奖励（即从i位置到最后的累积奖励）；</li>
<li>计算的 KL 散度、两个模型的打分均输入到奖励函数中，计算奖励： loss=−(klctl⋅rKL+γ⋅Vt+1−Vt)log⁡P(At∣Vt)<em>l**oss</em>=−(<em>k<strong>l</strong>c**tl</em>⋅<em>r<strong>K</strong>L</em>+<em>γ</em>⋅<em>V**t</em>+1−<em>V**t</em>)log<em>P</em>(<em>A**t</em>∣<em>V**t</em>) ，这里的 klctl<em>k<strong>l</strong>c**tl</em> 是控制 KL 散度对结果影响的权重参数， γ<em>γ</em> 是控制下一个时间（也就是样本）打分对结果影响的权重参数， Vt<em>V**t</em> 是 Critic Model 的打分输出， At<em>A**t</em> 则是 Reward Model 的打分输出;</li>
<li>根据奖励函数分别计算出的 actor loss 和 critic loss，更新 Actor Model 的参数和 Critic Model 的参数；注意，Actor Model 和 Critic Model 的参数更新方法是不同的，此处就不再一一赘述了，感兴趣的读者可以深入研究强化学习的相关理论。</li>
</ol>
<p>在上述过程中，因为要使用到四个模型，显存占用会数倍于 SFT。例如，如果我们 RM 和 LLM 都是用 7B 的体量，PPO 过程中大概需要 240G（4张 80G A100，每张卡占用 60G）显存来进行模型加载。那么，为什么我们需要足足四个模型呢？Actor Model 和 Critic Model 较为容易理解，而之所以我们还需要保持原参数不更新的 Ref Model 和 Reward Model，是为了限制模型的更新不要过于偏离原模型以至于丢失了 Pretrain 和 SFT 赋予的能力。</p>
<p>当然，如此大的资源占用和复杂的训练过程，使 RLHF 成为一个门槛非常高的阶段。也有学者从监督学习的思路出发，提出了 DPO（Direct Preference Optimization，直接偏好优化），可以低门槛平替 RLHF。DPO 的核心思路是，将 RLHF 的强化学习问题转化为监督学习来直接学习人类偏好。DPO 通过使用奖励函数和最优策略间的映射，展示了约束奖励最大化问题完全可以通过单阶段策略训练进行优化，也就是说，通过学习 DPO 所提出的优化目标，可以直接学习人类偏好，而无需再训练 RM 以及进行强化学习。由于直接使用监督学习进行训练，DPO 只需要两个 LLM 即可完成训练，且训练过程相较 PPO 简单很多，是 RLHF 更简单易用的平替版本</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
