当前位置: 首页 > article >正文

[LLM] 大模型基础|预训练|有监督微调SFT | 推理

现在的大模型在进行预训练时大部分都采用了GPT的预训练任务,即 Next token prediction。

要理解大语言模型(LLM),首先要理解它的本质,无论预训练、微调还是在推理阶段,核心都是next token prediction,也就是以自回归的方式从左到右逐步生成文本。

基本概念:

  • Token:在NLP中,一个“token”可以是一个词、一个字或一个标点符号。例如,句子“我爱北京”被切分成三个tokens: “我”, “爱” 和 “北京”。
  • Prediction:预测是指根据模型的当前输入,猜测接下来应该出现的token是什么。

模型训练

  • 在训练过程中,模型通过大量的文本数据来学习文本之间的模式和结构。
  • 例如,模型会看到成千上万次的“我爱X”这样的模式,其中X可以是各种不同的词。通过这样的训练,模型会学会哪些词最有可能出现在“我爱”之后。

要理解清楚这一训练过程,最主要的就是搞清楚预训练的数据怎么构造,数据怎么喂给模型,模型输出的是什么,以及如何计算loss

什么是token?

token是指文本中的一个词或者子词,给定一句文本,送入语言模型前首先要做的是对原始文本进行tokenize,也就是把一个文本序列拆分为离散的token序列

其中,tokenizer是在无标签的语料上训练得到的一个token数量固定且唯一的分词器,这里的token数量就是大家常说的词表,也就是语言模型知道的所有tokens。

当我们对文本进行分词后,每个token可以对应一个embedding,这也就是语言模型中的embedding层,获得某个token的embedding就类似一个查表的过程

我们知道文本序列是有顺序的,而常见的语言模型都是基于注意力机制的transformer结构,无法自动考虑文本的前后顺序,因此需要手动加上位置编码,也就是每个位置有一个位置embedding,然后和对应位置的token embedding进行相加

在模型训练或推理阶段大家经常会听到上下文长度这个词,它指的是模型训练时接收的token训练的最大长度,如果在训练阶段只学习了一个较短长度的位置embedding,那模型在推理阶段就不能够适用于较长文本(因为它没见过长文本的位置编码)

语言模型的预训练

当我们有了token embedding和位置embedding后,将它们送入一个decoder-only的transofrmer模型,它会在每个token的位置输出一个对应的embedding(可以理解为就像是做了个特征加工)

有了每个token的一个输出embedding后,我们就可以拿它来做next token prediction了,其实就是当作一个分类问题来看待

  • 首先我们把输出embedding送入一个线性层,输出的维度是词表的大小,就是让预测这个token的下一个token属于词表的“哪一类”
  • 为了将输出概率归一化,需要再进行一个softmax变换
  • 训练时就是最大化这个概率使得它能够预测真实的下一个token
  • 推理时就是从这个概率分布中采样下一个token

训练阶段:因为有causal自注意力的存在,我们可以一次性对一整个句子每个token进行下一个token的预测,并计算所有位置token的loss,因此只需要一forward

通过一个完整的例子来介绍一下这个过程,假设现在有一个用来预训练的数据集

你知道什么是预训练吗?

假设经过分词后

你: 9
知道: 3
什么: 6
是: 4
预训练: 2
吗: 1
?: 5

原来的数据变为如下序列,后面补了三个0(假设我们希望最大序列长度是10)

9 3 6 4 2 1 5 0 0 0

预测下一个token就类似于9预测3,9 3预测6,以此类推,但是如果这样拆成很多个数据段其实比较低效,因此就可以考虑移位来构造数据,即

  • 模型输入X为          9 3 6 4 2 1 5 0 0 0
  • 模型输出targets为  3 6 4 2 1 5 0 0 0 0

这样就可以一次性把整条序列喂给模型,计算一次就包含了6个预测下一个token的损失了。

注意这里模型的设计是有讲究的,我们不能让输入看到后面的词(如果看得到的话就没必要进行预测了),也就是“你”在模型内看不到“知道”,“你 知道”在模型内看不到“什么”,这个可以通过注意力机制实现,不是本文的关注点,这里就不展开了。

现在模型的输入的维度为(1,10),第一维为batch_size,然后经过embedding层后变为(1,10,768),这里假设embedding的维度为768。

记住进入transfomer前后数据的维度不会发生变化,把transfomer当作一个黑盒,也就是transformer(X)的维度还是(1,10,768),接下来就是基于它来进行预测了,因为要预测哪个词,词的可能情况就是词表的大小,所以做的就是一个分类任务,预测下一个token是词表中的哪一个(词表中的每一个词当作一个类别)。

为了完成分类任务,需要对transformer的输出做一个映射,映射到跟词表一样大,也就需要定义这样一个线性变换

output_layer = nn.Linear(768, vocab_size, bias=False)

然后logits=output_layer(transformer(X))的维度就是(1,10,vocab_size),接下来就可以计算loss了,具体来说就是计算logits(模型预测)与targets(真实标签)之间的交叉熵损失,同时忽略了填充值对应的损失。

语言模型的的Supervised fine-tuning (SFT)阶段

“有监督微调”意味着使用有标签的数据来调整一个已预训练好的语言模型(LLM),使其更适应某一特定任务。通常LLM的预训练是无监督的,但微调过程往往是有监督的。

当进行有监督微调时,模型权重会根据与真实标签的差异进行调整。通过这个微调过程,模型能够捕捉到标签数据中特定于某一任务的模式和特点。使得模型更加精确,更好地适应某一特定任务。

以一个简单的例子来说,你有一个已经预训练好的LLM。当输入“我不能登录我的账号,我该怎么办?”时,它可能简单地回答:“尝试使用‘忘记密码’功能来重置你的密码。”

这个回答很直接,适用于一般问题,但如果是客服场景,可能就不太合适了。一个好的客服回答应该更有同情心,并且可能不会这么直接,甚至可能包含联系信息或其他细节。这时候,有监督微调就显得非常重要了。

经过有监督微调后,你的模型可以提供更加符合特定指导原则的答案。例如,经过一系列专业的培训示例后,你的模型可以更有同情心地回答客服问题。

接下来我们还是从数据到模型输出,计算loss的步骤来看看SFT的实现原理。

首先还是来看看数据怎么构造,SFT的每一条样本一般由两部分组成,也就是prompt(instruction)+ answer,比如:

  • prompt: 翻译以下句子: What is pretrain
  • answer: 什么是预训练

也就是我们要给模型提供一些类似于问答形式的答案来学习,有了前面预训练的经验后,SFT其实就很好理解的,它本质上也在做next token prediction,只是我们更希望模型关注answer部分的预测,这可以通过生成一个mask向量来屏蔽不希望计算loss的部分,下面就是数据构造的一个示意:做的事情就是拼接prompt和answer,并在answer两侧添加一个开始和结束的符号,算一下prompt/instruction的长度,以及后面需要pad的长度,然后生成一个mask向量,answer部分为1,其他部分为0。

input_id=prompt+[bos]+answer+[eos]
context_length = input_id.index(bos)
mask_position = context_length - 1
pad_len = max_length - len(input_id)
input_id = input_id + [pad] * pad_len
loss_mask = [0]*context_length+[1]*(len(input_id[mask_position+1:])) + [0]*pad_len

构造好输入后,token转为embedding,经过transformer的过程跟之前预训练完全一样,也就是我们又得到了一个维度是(1,10,vocab_size)的输出logits=output_layer(transformer(X)),进一步就可以计算answer部分的loss了,其实就是通过mask把不希望考虑的地方乘以0,保留answer部分loss。

loss = F.cross_entropy(logits.view(-1, logits.size(-1)), Y.view(-1), ignore_index=0,reduce=False)
loss_mask = loss_mask.view(-1)
loss = torch.sum(loss*loss_mask)/loss_mask.sum()

有了loss,进行反向传播更新模型参数就OK啦。

Reference:Supervised Fine-tuning: customizing LLMs

语言模型的推理阶段

以自回归的方式进行预测

  • 每次预测下一个token
  • 将预测的token拼接到当前已经生成的句子上
  • 再基于拼接后的句子进行预测下一个token
  • 不断重复直到结束

其中,在预测下一个token时,每次我们都有一个概率分布用于采样,根据不同场景选择采样策略会略有不同,不然有贪婪策略、核采样、Top-k采样等,另外经常会看到Temperature这个概念,它是用来控制生成的随机性的,温度系数越小越稳定。

Q&A

Q:预训练的数据是纯文本吧?模型输出应该和什么去对比计算这个loss?纯文本就没有一个类似于sft的output/answer的标签吧?这个时候target是啥呢?
A:就是下一个词预测,和下一个词去计算loss

Q:词表的大小就是语料库大小吧
A:不是的,是分词器的token总数

Q:会不会生成和 标准答案长度不一样的 答案呢?这样的画怎么计算loss?
A:逐个token生成和计算的哈;训练是逐个token预测的。 推理没有长度限制

Q:SFT时,question部分也是预测出来的?如果预测的结果与input不一致怎么办?我理解question部分应该直接用input去提取特征,然后去预测answer部分。
A:不一致是正常的,不计算loss即可,包括answer也是不一致的,但是一定会计算loss;
A:会预测,不一定需要计算loss,业界做法有的会计算loss有的不会

Q:请问SFT样本的prompt可以是图片+文本的形式吗
A:可以的,可以接一个图像encoder,然后把图像encoder的输出拼在文本后面

Q:请问LLM训练过程中,如果因为设置的max_len过大导致padding过多,会有什么影响吗
A:不会计算loss就不影响,只是浪费计算,Padding不参与计算;Padding不会计算loss。

代码实现

下面代码来自项目https://github.com/karpathy/nanoGPT/tree/master,同样是一个很好的项目,推荐初学者可以看看。

对于各种基于Transformer的模型,它们都是由很多个Block堆起来的,每个Block主要有两个部分组成:

  • Multi-headed Causal Self-Attention
  • Feed-forward Neural Network

结构的示意图如下:

看图搭一下单个Block

然后看下一整个GPT的结构

主要就是两个embedding层(token、位置)、多个block、一些额外的dropout和LayerNorm层,以及最后用来预测下一个token的线性层。说破了就是这么简单。

这边还用到了weight tying的技巧,就是最后一层用来分类的线性层的权重和token embedding层的权重共享。

接下来重点来关注一下训练和推理的forward是如何进行的,这能帮助大家更好的理解原理。

首先需要构建token embedding和位置embedding,把它们叠加起来后过一个dropout,然后就可以送入transformer的block中了。

需要注意的是经过transforemr block后出来的tensor的维度跟之前是一样的。拿到每个token位置对应的输出embedding后,就可以通过最后的先行层进行分类,然后用交叉熵损失来进行优化。

再看一下完整的过程,其中只需要将输入左移一个位置就可以作为target了

接下来看推理阶段:

  • 根据当前输入序列进行一次前向传播
  • 利用温度系数对输出概率分布进行调整
  • 通过softmax进行归一化
  • 从概率分布进行采样下一个token
  • 拼接到当前句子并再进入下一轮循环

大模型基础|预训练|有监督微调SFT - 知乎 (zhihu.com)

从原理到代码理解语言模型训练和推理 - 知乎 (zhihu.com)


http://www.kler.cn/a/274526.html

相关文章:

  • Idean 处理一个项目引用另外一个项目jar 但jar版本低的问题
  • druid与pgsql结合踩坑记
  • 使用Vscode+EIDE+Jlink开发STM32环境配置教程
  • go面试问题
  • JavaWeb期末复习(习题)
  • 消息队列 Kafka 架构组件及其特性
  • node.js 的常用命令
  • SLAM 求解IPC算法
  • SQL的INSERT IGNORE用法
  • .NET 异步编程(异步方法、异步委托、CancellationToken、WhenAll、yield)
  • 图像分割在疾病诊断中的应用案例
  • 无法加载DLL“SQLite.Interop.dll“:找不到指定模块
  • Linux作业
  • 键盘映射工具KeyTweak的使用,把F9和F10改为 Home、End
  • [PwnThyBytes 2019]Baby_SQL
  • Golang 开发实战day05 - Loops(1)
  • 【智能家居】东胜物联提供软硬一体化智能家居解决方案,助企业提高市场占有率
  • 【计算机网络_网络层】IP协议
  • 卸载.Net SDK
  • ClickHouse列式存储基础笔记
  • BUUCTF-Misc10
  • 搭建基于 Snowflake 的 CI/CD 最佳实践!
  • 【Linux】进程排队的理解进程状态的表述僵尸进程和孤儿进程的理解
  • 六种GPU虚拟化:除了直通、全虚拟化 (vGPU)还有谁?
  • Spark 3.5.0 特性速览
  • 安装tensorflow2.4