本文是github上的大模型教程LLMs-from-scratch的学习笔记,教程地址:教程链接
STAGE 1: BUILDING
1. 数据准备与采样
LLM的预测过程,是一个不断预测下一个词(准确的说是token
)的过程,每次根据输入的内容,预测下一个词,然后将新的句子重新丢入模型预测,得到下下个输出,重复这个过程,直到模型输出结束标志。
正如上面所说,准确来说,大模型不是预测下一个单词,而是预测下一个token
,token
是模型预测输出的单位,可能不止一个单词,也可能小于一个单词(例如先输出一个单词的前半部分,然后根据前文再推导出单词后缀)。
tokenization
是大模型中重要的一部分,决定了模型如何将输入的句子拆分开,根据拆分后输出的token
,预测下一个token
。
有许多tokenizer
,例如可以简单地根据空格拆分句子,或者GPT使用的BPE tokenizer
- 人为定义的Tokenizer:
- 规则型Tokenizer:这种类型的tokenizer通常是基于一些预定义的规则,比如空格、标点符号等来切分文本。例如,简单的空格分割就是基于空格将句子切分成单词。这种tokenizer是人为定义的,不需要训练。
- 训练得到的Tokenizer:
- 基于统计的Tokenizer:这种tokenizer会基于大量的文本数据统计信息来决定最佳的切分点。例如,字节对编码(Byte Pair Encoding, BPE)、WordPiece等算法,它们会通过训练数据来学习如何将单词切分成子词单元。这种tokenizer需要通过训练过程来优化其切分规则。
简单的说,一个tokenizer就是一本词典,告诉预处理的时候,输入的句子要如何拆分成一个一个token,并且tokenizer提供了每个token对应的索引位置,这些索引通常被用作查找表(look-up table)中的键,以获取token的嵌入向量(embedding vector)。嵌入向量是token在连续向量空间中的表示,它们通常是通过训练得到的,并且能够捕获token的语义信息。
嵌入矩阵(embedding matrix)是一个大型的矩阵,其中每一行对应词汇表中一个token的嵌入向量。
如何处理没有见过的词:正如上图所示,tokenizer包含很小的词组,因此一个大的单词,即使是一个随便拼的单词,也能被拆分成许多小的token组合而成,但是准确率和效率可能不高。
2. 模型架构
蓝色部分就是transformer
,LLM通过重复这一模块,以及在每一层使用多个注意力头来扩大模型规模。(每一层有多个transformer,然后重复多层)
关于transformer
的介绍可以阅读其他博客。
STAGE 2: PRETRAINING
类似GPT和LLAMA,都采用了自回归模型来预训练。
自回归训练(Autoregressive Training)是一种用于语言模型(如GPT)的训练方法,旨在让模型通过学习上下文来预测序列中的下一个token。该方法是生成模型的重要组成部分,特别适用于文本生成任务。下面详细介绍自回归训练的过程及其背后的原理。
自回归模型的基本思想是通过递归的方式生成序列中的每个token。具体来说,模型从序列的第一个token开始,通过观察当前已经生成的部分,逐步预测下一个token,直到生成完整的序列。
数学上,自回归模型的目标是通过给定先前的tokens来估计下一个token的条件概率,即:
P ( x t ∣ x 1 , x 2 , … , x t − 1 ) P(x_t | x_1, x_2, \dots, x_{t-1}) P(xt∣x1,x2,…,xt−1)
2.1 数据准备
- 输入数据:预训练使用的文本数据通常是未经标注的自然语言文本。训练时,数据会被token化成一个个离散的单位(例如单词、子词或字符)。
- 序列处理:文本数据被分割成固定长度的序列(例如512个token)。每个序列会被用作模型的输入,其中部分token将被用于预测下一个token。
2.2 模型输入和输出
- 输入序列:输入序列通常是一个连续的文本片段,例如
["The", "cat", "is", "on", "the", "mat"]
。 - 目标序列:目标序列是输入序列右移一个位置后的版本,模型的目标是基于输入序列预测目标序列的每个token。例如,输入
["The", "cat", "is", "on", "the"]
的目标序列是["cat", "is", "on", "the", "mat"]
。
2.3 损失函数
- 交叉熵损失:训练时,模型生成的每个token的概率分布与目标token的真实分布之间的差异由交叉熵损失函数来衡量。公式为:
Loss = − ∑ t = 1 T log P ( y t ∣ x 1 , … , x t − 1 ) \text{Loss} = -\sum_{t=1}^{T} \log P(y_t | x_1, \dots, x_{t-1}) Loss=−t=1∑TlogP(yt∣x1,…,xt−1)
其中, y t y_t yt 是目标token, P ( y t ∣ x 1 , … , x t − 1 ) P(y_t | x_1, \dots, x_{t-1}) P(yt∣x1,…,xt−1) 是模型预测的目标token的概率。
STAGE 3: FINETUNING
在预训练好的模型上进行微调,根据微调的目的,可以有两种情况,一种是基于分类任务,一种是基于指令任务。
1. 分类任务
分类任务较为简单,只需要将模型的最后一层全连接层(例如图中的768到50257,50257是tokenizer的词汇量)的维度转换为分类任务的维度,例如一个二分类任务,我们替换掉最终的50257的词汇表查找维度,改为2个维度的分类任务即可。
微调的时候不需要微调全部的参数,作者指出,随着微调层数的增多,微调任务的准确率没有显著上升,并且会带来更多的微调耗时。
2. 指令微调
相比于分类任务,大家更关心的可能是指令微调,如何构建一个私人,适合下游子任务的交互大模型。
通过对应任务要求,给出指令,输入,输出的数据集,微调大模型在特定任务上的能力。
与预训练不同,预训练是一个无监督训练的过程,不需要标签,给定一个文本后,只需要不断地做next token prediction就可以,指令微调是一个有监督的训练过程。
在指令微调过程中,损失的计算主要依赖于生成的输出序列与目标序列(即期望响应)之间的差异。通常使用交叉熵损失来衡量模型生成的每个token与目标序列中的对应token之间的差异。
- 输入序列:包含指令(或提示),例如:“Translate the following English sentence to French: ‘Hello, how are you?’”
- 目标序列:包含期望的响应,例如:“Bonjour, comment ça va?”
损失计算过程
- Token化:输入序列和目标序列首先被token化,即被分解为一个个离散的token。
- 模型预测:模型基于输入序列生成一个输出序列。在训练时,模型逐个token生成预测结果。
- 交叉熵损失:
- 对于每一个生成的token,计算它与目标token之间的交叉熵损失。
- 对于整个序列,交叉熵损失的公式为:
Loss = − 1 T ∑ t = 1 T log P ( y t ∣ X , y 1 , … , y t − 1 ) \text{Loss} = -\frac{1}{T} \sum_{t=1}^{T} \log P(y_t | X, y_1, \ldots, y_{t-1}) Loss=−T1t=1∑TlogP(yt∣X,y1,…,yt−1)
其中 T 是序列的长度, P ( y t ∣ X , y 1 , … , y t − 1 ) P(y_t | X, y_1, \ldots, y_{t-1}) P(yt∣X,y1,…,yt−1)是模型预测的token y t y_t yt 的概率。 - 换句话说,对于序列中的每个token,模型计算生成该token的概率(基于先前的上下文),然后计算模型输出的概率分布与目标分布之间的交叉熵。