LLaMA的解读与其微调(含LLaMA 2):Alpaca-LoRA/Vicuna/BELLE/中文LLaMA/姜
媒介近期,除了研究ChatGPT背后的各种技能细节 不断看论文(至少100篇,100篇目录见此:ChatGPT相关技能必读论文100篇),还开始研究一系列开源模子(包括各自对应的模子架构、训练方法、训练数据、本地私有化摆设、硬件设置要求、微调等细节)
本文一开始是作为此文《ChatGPT技能原理剖析:从RL之PPO算法、RLHF到GPT4、instructGPT》的第4部分,但随着研究深入 为避免该文篇幅又过长,将把『第4部分 开源项目』抽取出来 独立本钱文,然后不断续写本文直至成了一个系列
毕竟我上半年的目标之一,便是把ChatGPT涉及的所有一切关键技能细节,以及相关的开源项目都研究的透透的,故过程中会不断产出一篇篇新文章、新课程(比如七月类ChatGPT微调实战课)出来
第一部分 LLaMA的代码级解读:RMSNorm/SwiGLU/RoPE/Transformer
1.1 Meta发布LLaMA((7B 13B 33B 65B):参数少但多数任务的效果好于GPT3
一直致力于LLM模子研究的国外TOP 3大厂除了OpenAI、Google,便是Meta(原来的Facebook)
[*]Meta曾第一个发布了基于LLM的聊天呆板人——BlenderBot 3,但输出不够安全,很快下线;再厥后,Meta发布一个专门为科学研究设计的模子Galactica,但用户期望过高,发布三天后又下线
[*]23年2.24日,Meta通过论文《LLaMA: Open and Efficient Foundation Language Models》发布了自家的大型语言模子LLaMA(这是LLaMA的GitHub代码所在,这是解读之一),有多个参数规模的版本(7B 13B 33B 65B),并于次月3.8日被迫开源
LLaMA只使用公开的数据(总计1.4T即1.4万亿的token,其中CommonCrawl的数据占比67%,C4数据占比15%,Github、Wikipedia、Books这三项数据均都各自占比4.5%,ArXiv占比2.5%,StackExchange占比2%),论文中提到
When training a 65B-parameter model, our code processes around 380 tokens/sec/GPU on 2048 A100 GPU with 80GB of RAM.
This means that training over our dataset containing 1.4T tokens takes approximately 21 days
且试图证明小模子在足够多的的数据上训练后,也能达到甚至超过大模子的效果
[*]比如13B参数的版本在多项基准上测试的效果好于2020年的参数规模达175B的GPT-3
[*]而对于65B参数的LLaMA,则可与DeepMind的Chinchilla(70B参数)和谷歌的PaLM(540B参数)旗鼓相当
[*]且Meta还实验使用了论文「Scaling Instruction-Finetuned Language Models」中先容的指令微调方法,由此产生的模子LLaMA-I,在MMLU(Massive Multitask Language Understanding,大型多任务语言理解)上要优于Google的指令微调模子Flan-PaLM-cont(62B)
1.2 LLaMA的模子架构:改造Transformer——RMSNorm/SwiGLU/RoPE
1.2.1 项目情况依赖:torch、fairscale、fire、sentencepiece
此项目给出的情况依赖有4个:
[*]torch
[*]fairscale,fairscale是用来做GPU分布的,一样寻常是当使用DDP仍然遇到超显存的题目时使用fairscale
[*]fire,fire是一个下令行工具,用或者不用他都可以
[*]sentencepiece,sentencepiece是用于tokenizer的工具包
「 SentencePiece 实现了subword单位(例如,字节对编码(BPE)和 unigram语言模子),并可以直接从原始句子训练字词模子(subword model),这是对SentencePiece的解读:大模子词表扩充必备工具SentencePiece 」
首先,引入一些库和定义一个 Tokenizer 类 # 引入 sentencepiece 库的 SentencePieceProcessor 模块,用于进行分词操作
from sentencepiece import SentencePieceProcessor
# 引入 logging 库的 getLogger 模块,用于生成日志
from logging import getLogger
# 引入 typing 库的 List 模块,用于注释函数参数或返回值的类型
from typing import List
# 引入 os 库,提供了大量与操作系统进行交互的接口
import os
# 创建一个日志记录器
logger = getLogger()
# 定义一个 Tokenizer 类
class Tokenizer:
# 初始化函数,参数为 SentencePiece 模型的路径
def __init__(self, model_path: str):
# 判断指定的模型文件是否存在
assert os.path.isfile(model_path), model_path
# 加载 SentencePiece 模型
self.sp_model = SentencePieceProcessor(model_file=model_path)
# 记录日志,提示模型加载成功
logger.info(f"Reloaded SentencePiece model from {model_path}")
# 获取模型的词汇量、开始标记 ID、结束标记 ID、填充标记 ID
self.n_words: int = self.sp_model.vocab_size()
self.bos_id: int = self.sp_model.bos_id()
self.eos_id: int = self.sp_model.eos_id()
self.pad_id: int = self.sp_model.pad_id()
# 记录日志,显示获取的信息
logger.info(
f"#words: {self.n_words} - BOS ID: {self.bos_id} - EOS ID: {self.eos_id}"
)
# 确保模型的词汇量与词片段大小一致
assert self.sp_model.vocab_size() == self.sp_model.get_piece_size() 然后再定义encode、decode函数
# 编码函数,将输入的字符串编码为 token id 列表
def encode(self, s: str, bos: bool, eos: bool) -> List:
# 检查输入的是否是字符串
assert type(s) is str
# 使用 SentencePiece 模型将字符串编码为 token id 列表
t = self.sp_model.encode(s)
# 如果需要在开头添加开始标记,就将开始标记 id 添加到列表的开头
if bos:
t = + t
# 如果需要在结尾添加结束标记,就将结束标记 id 添加到列表的结尾
if eos:
t = t +
# 返回 token id 列表
return t
# 解码函数,将 token id 列表解码为字符串
def decode(self, t: List) -> str:
# 使用 SentencePiece 模型将 token id 列表解码为字符串
return self.sp_model.decode(t)
1.2.2 RMSNorm:对每个Transformer子层的输入举行归一化
为了提高训练的稳固性,对每个transformer子层的输入举行归一化,而不是对输出举行归一化,且使用由Zhang和Sennrich(2019)提出的RMSNorm(Root Mean Square Layer Normalization)
RMS Norm是一样寻常LayerNorm的一种变体,可以在梯度降落时令损失更加平滑,与layerNorm相比,RMS Norm的主要区别在于去掉了减去均值的部分(re-centering),只保留方差部分(re-scaling)
为一览无余,我们看下它们各自的归一化的表达式
[*]LayerNorm
在给定一个输入特征向量https://latex.csdn.net/eq?a_i后,先计算 x 的均值 https://latex.csdn.net/eq?%5Cmu 和标准差 https://latex.csdn.net/eq?%5Csigmahttps://latex.csdn.net/eq?%5Cmu%20%3D%20%5Cfrac%201%20n%20%5Csum_%7Bi%3D1%7D%5Ena_i
https://latex.csdn.net/eq?%5Csigma%3D%20%5Csqrt%20%7B%5Cfrac%201%20n%20%5Csum_%7Bi%3D1%7D%5En%7B%7B%28a_i-%5Cmu%29%7D%5E2%7D%7D
然后举行归一化操纵:https://latex.csdn.net/eq?%5Coverline%7Ba%7D_i%20%3D%20%5Cfrac%20%7Ba_i-%20%5Cmu%7D%20%5Csigma%20g_i%20+%20b_i
其中的
https://latex.csdn.net/eq?%5Crightarrow https://latex.csdn.net/eq?g_i是可学习的缩放参数,来调整每个特征在归一化后的标准或权重,最终作用是规复归一化操纵大概损失的信息,如数据的比例和分布等
https://latex.csdn.net/eq?%5Crightarrow 而https://latex.csdn.net/eq?b_i是偏移因子,可以对归一化并放缩后的数据举行偏移,使模子可以学习到一个最优的数值范围,比如在ReLU激活函数中,我们大概希望值在0以上
[*]RMS Norm
首先,计算输入特征向量 a 的平方根均值 (其中,n是向量a的元素数量) https://latex.csdn.net/eq?%7BRMS%28a%29%7D%3D%5Csqrt%20%7B%5Cfrac%201%20n%20%5Csum_%7Bi%3D1%7D%5En%7B%7Ba_i%7D%5E2%7D%7D
然后,对输入特征向量 a 举行归一化 https://latex.csdn.net/eq?%5Coverline%7Ba%7D_i%20%3D%20%5Cfrac%20%7Ba_i%7D%20%7BRMS%28a%29%7D%20g_i
此外,可选地,RMSNorm 还可以引入可学习的放缩参数 https://latex.csdn.net/eq?g_i 和偏移参数 https://latex.csdn.net/eq?b_i:https://latex.csdn.net/eq?%5Coverline%7Ba%7D_i%20%3D%20%5Cfrac%7Ba_i%7D%20%7BRMS%28a%29%7D%20g_i%20+%20b_i
其代码实现为 class RMSNorm(torch.nn.Module):
def __init__(self, dim: int, eps: float = 1e-6):
super().__init__()
// eps防止取倒数之后分母为0
self.eps = eps
self.weight = nn.Parameter(torch.ones(dim))
// x是输入
def _norm(self, x):
// torch.rsqrt是开平方并取倒数
// x.pow(2)是平方
/ mean(-1)是在最后一个维度(即hidden特征维度)上取平均
return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)
def forward(self, x):
output = self._norm(x.float()).type_as(x)
// weight是末尾乘的可训练参数,即gi
return output * self.weight 至于RMS Norm为什么有效,需要求梯度举行分析,感兴趣的同砚可以阅读RMS Norm的论文
1.2.3 SwiGLU替代ReLU
为了更好的理解SwiGLU,首先你得先了解什么是ReLU和GLU
[*]ReLU的函数表达式为https://latex.csdn.net/eq?f%28x%29%20%3D%20max%280%2Cx%29,这意味着对于所有负的输入值,ReLU函数的输出都是0,对于所有正的输入值,ReLU函数的输出等于输入值自己
[*]GLU 的基本思想是引入一种称为“门”机制,该机制可以动态地控制信息的流动
https://latex.csdn.net/eq?GLU%28x%29%20%3D%20x%20%5Cotimes%20%5Csigma%20%28g%28x%29%29
这个公式意味着,对于每个输入 x,都会有一个相应的门值,这个门值由 sigmoid 函数https://latex.csdn.net/eq?%5Csigma%20%28g%28x%29%29产生,其范围在 0 到 1 之间(在正数区域接近于1,负数区域接近于0),这个门值用于调节相应的输入值
https://latex.csdn.net/eq?%5Crightarrow 如果 https://latex.csdn.net/eq?%5Csigma%20%28g%28x%29%29接近 1,那么“门”就几乎完全开启,输入 x 的信息能够自由流动,于是 GLU 的输出接近于 x
https://latex.csdn.net/eq?%5Crightarrow 如果 https://latex.csdn.net/eq?%5Csigma%20%28g%28x%29%29接近 0,意味着“门”几乎完全关闭,即输入 x 的大部分或全部信息被阻止通过,于是 GLU 的输出接近 0
而LLaMA采用Shazeer(2020)提出的SwiGLU替换了原有的ReLU,SwiGLU的作用机制是根据输入数据的特性,通过学习到的参数自动调整信息流动的路径,具体是采用SwiGLU的Feedforward Neural Network (简称FNN,即使用可学习的门控机制的前馈神经网络)
其在论文中以如下公式举行表述:
https://latex.csdn.net/eq?FFN_%7BswiGLU%7D%28x%2C%20W%2C%20V%2C%20W_2%29%20%3D%20%28Swish_%5Cbeta%28xW%29%5Cotimes%20xV%29W_2
解释下这个公式
[*]该公式先是通过Swish非线性激活函数处理 “输入https://latex.csdn.net/eq?x和权重矩阵https://latex.csdn.net/eq?W的乘积”
[*]上面步骤1得到的效果和 “输入https://latex.csdn.net/eq?x与权重矩阵https://latex.csdn.net/eq?V的乘积” 举行逐元素的乘法
这个操纵相当于在 Swish 激活的输出和第二个线性变换的输出之间引入了一个类似于GLU的“门”,这个门的值是由原始输入 https://latex.csdn.net/eq?x 通过线性变换 https://latex.csdn.net/eq?V计算得到的,因此,它可以动态地控制 Swish 激活的输出
[*]最后乘以权重矩阵https://latex.csdn.net/eq?W_2
至于Swish激活函数可表现为
https://latex.csdn.net/eq?Swish_%5Cbeta%28x%29%20%3D%20x%5Csigma%28%5Cbeta%20x%29
https://latex.csdn.net/eq?%5Csigma表现sigmoid函数,但其输入被缩放了https://latex.csdn.net/eq?%5Cbeta 倍,https://latex.csdn.net/eq?%5Cbeta是一个可以学习的参数,比如下图,https://latex.csdn.net/eq?%5Cbeta差别,Swish激活函数的形状则各异
https://i-blog.csdnimg.cn/blog_migrate/f80efcf308afe035abca2a244bd25432.png
[*]当 https://latex.csdn.net/eq?%5Cbeta 趋近于 0 时,Swish 函数趋近于线性函数 y = x
[*]当 https://latex.csdn.net/eq?%5Cbeta 趋近于无穷大时,Swish 函数趋近于 ReLU 函数
对应论文见:Ramachandran et al., 2017
代码实现上:可以通过调用torch内置方法F.silu()实现,会在下文的FFN部分先容
为增长大家对SwiGLU的理解,我还是举个简单的例子来阐明这个过程
https://latex.csdn.net/eq?FFN_%7BswiGLU%7D%28x%2C%20W%2C%20V%2C%20W_2%29%20%3D%20%28Swish_%5Cbeta%28xW%29%5Cotimes%20xV%29W_2
https://latex.csdn.net/eq?Swish_%5Cbeta%28x%29%20%3D%20x%5Csigma%28%5Cbeta%20x%29%29假设我们的输入 x 是一个二维向量 ,权重矩阵 W 和 V 都是 2x2 矩阵,且我们简化题目,令 β =1
[*]先计算出来xW的效果,即x乘以权重矩阵 W 得到新的向量z,假设z是
[*]对 xW的效果 z = 应用 Swish 激活函数,即Swish_1(z) = z ⊙ σ(z) =
[*]然后,我们计算 xV 以得到“门”控制值
计算 xV 得到新的向量 y,假设 y =
[*]接着,我们将 Swish_1(xW) 和 xV 做元素级别的乘法,也就是实施"门控":
(Swish_1(xW) ⊙ xV) = =
在这个例子中,我们可以看到 xV 的输出 在元素级别上控制了 Swish_1(xW) 的输出
[*]第一个维度的门值为 1,因此 Swish_1(xW) 的第一个维度的输出能够“通过”,得到进入门控之前的效果5σ(5)
[*]第二个维度的门值为 0,因此 Swish_1(xW) 的第二个维度的输出被“阻止”了,效果为 0
这就是“门”的动态控制作用:它根据 xV 的输出调整 Swish_1(xW) 的输出,通过这种方式,模子可以根据输入 x 的差别,动态地调整信息流动
1.2.4 位置编码:RoPE
关于旋转位置编码的理解,请参看此文《一文通透位置编码:从标准位置编码、旋转位置编码RoPE到ALiBi、LLaMA 2 Long(含NTK-aware简介)》,足够详尽且细致
接下来,我们来看下LLaMA里是怎么实现这个旋转位置编码的,具体而言,LLaMA 的model.py文件内里实现了旋转位置编码(为方便大家理解,我给相关代码 加了下解释)
首先,逐一实现这三个函数
[*]precompute_freqs_cis # 预计算频率和复数的函数
def precompute_freqs_cis(dim: int, end: int, theta: float = 10000.0):
freqs = 1.0 / (theta ** (torch.arange(0, dim, 2)[: (dim // 2)].float() / dim)) # 计算频率
t = torch.arange(end, device=freqs.device) # 根据结束位置生成序列
freqs = torch.outer(t, freqs).float() # 计算外积得到新的频率
freqs_cis = torch.polar(torch.ones_like(freqs), freqs) # 计算复数
return freqs_cis # 返回复数
[*]reshape_for_broadcast # 重塑的函数
def reshape_for_broadcast(freqs_cis: torch.Tensor, x: torch.Tensor):
ndim = x.ndim # 获取输入张量的维度
assert 0 <= 1 < ndim # 检查维度的合理性
assert freqs_cis.shape == (x.shape, x.shape[-1]) # 检查复数的形状
shape = # 计算新的形状
return freqs_cis.view(*shape) # 重塑复数的形状并返回
[*]apply_rotary_emb # 应用旋转嵌入的函数
def apply_rotary_emb(
xq: torch.Tensor,
xk: torch.Tensor,
freqs_cis: torch.Tensor,
) -> Tuple:
xq_ = torch.view_as_complex(xq.float().reshape(*xq.shape[:-1], -1, 2)) # 将xq视为复数
xk_ = torch.view_as_complex(xk.float().reshape(*xk.shape[:-1], -1, 2)) # 将xk视为复数
freqs_cis = reshape_for_broadcast(freqs_cis, xq_) # 重塑复数的形状
xq_out = torch.view_as_real(xq_ * freqs_cis).flatten(3) # 计算xq的输出
xk_out = torch.view_as_real(xk_ * freqs_cis).flatten(3) # 计算xk的输出
return xq_out.type_as(xq), xk_out.type_as(xk) # 返回xq和xk的输出
之后,在注意力机制的前向传播函数中调用上面实现的第三个函数 apply_rotary_emb,赋上位置信息 (详见下文1.2.5节)
# 对Query和Key应用旋转嵌入
xq, xk = apply_rotary_emb(xq, xk, freqs_cis=freqs_cis) 1.3 Transform架构的实现:Attention计算、SA、FFN
LLaMA和GPT一样,都是基于Transformer这个架构,通常,我们在构建transformer时,是按Block构建的,每个transformer Block包含SA和FFN两部分,然后再通过堆叠block的情势,构建起整个transformer网络,LLaMA也是这样做的
1.3.1 Attention计算的总过程
回顾一下Attention计算的总体过程是(至于更多细节,详见此文:Transformer通俗笔记:从Word2Vec、Seq2Seq逐步理解到GPT、BERT):
[*]输入https://latex.csdn.net/eq?x,分别经过三个Linear得到https://latex.csdn.net/eq?x_q%2C%20x_k%2C%20x_v
[*]在 https://latex.csdn.net/eq?x_q 和https://latex.csdn.net/eq?x_k中加入旋转位置编码
[*]缓存 https://latex.csdn.net/eq?x_q 和 https://latex.csdn.net/eq?x_k
[*]计算https://latex.csdn.net/eq?softmax%28%5Cfrac%20%7BQK%5ET%7D%20%7B%5Csqrt%7Bd_k%7D%7D%29V
其中有一个细节就是缓存机制,它设计的目的是在generate时减少token的重复计算。简单解释一下,就是在计算第n个token特征的时候,需要用到第https://latex.csdn.net/eq?1%2C...%2Cn-1个token,即每次天生时,需要知道前面所有的过往信息,如果每次都从头算的话,那就会造成极大的浪费,所以就没算一个位置的信息,就把它缓存下来
接下来,我们来看下代码实现,首先是SA(self-attention)部分:
[*]先初始化,涉及到注意力计算中的三个query、key、value向量的产生 class Attention(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
# 设置本地注意力头的数量
self.n_local_heads = args.n_heads // fs_init.get_model_parallel_world_size()
# 每个注意力头的维度
self.head_dim = args.dim // args.n_heads
# Query投影层
self.wq = ColumnParallelLinear(
args.dim,
args.n_heads * self.head_dim,
bias=False,
gather_output=False,
init_method=lambda x: x,
)
# Key投影层
self.wk = ColumnParallelLinear(
args.dim,
args.n_heads * self.head_dim,
bias=False,
gather_output=False,
init_method=lambda x: x,
)
# Value投影层
self.wv = ColumnParallelLinear(
args.dim,
args.n_heads * self.head_dim,
bias=False,
gather_output=False,
init_method=lambda x: x,
)
# 输出投影层
self.wo = RowParallelLinear(
args.n_heads * self.head_dim,
args.dim,
bias=False,
input_is_parallel=True,
init_method=lambda x: x,
)
# 使用零初始化键缓存
self.cache_k = torch.zeros(
(args.max_batch_size, args.max_seq_len, self.n_local_heads, self.head_dim)
).cuda()
# 使用零初始化值缓存
self.cache_v = torch.zeros(
(args.max_batch_size, args.max_seq_len, self.n_local_heads, self.head_dim)
).cuda()
[*]后forward,其中的焦点便是https://latex.csdn.net/eq?softmax%28%5Cfrac%20%7BQK%5ET%7D%20%7B%5Csqrt%7Bd_k%7D%7D%29%20%5Ctimes%20V def forward(self, x: torch.Tensor, start_pos: int, freqs_cis: torch.Tensor, mask: Optional): bsz, seqlen, _ = x.shape # 举行Query投影 xq, xk, xv = self.wq(x), self.wk(x), self.wv(x) # 将形状调整为 xq = xq.view(bsz, seqlen, self.n_local_heads, self.head_dim) xk = xk.view(bsz, seqlen, self.n_local_heads, self.head_dim) xv = xv.view(bsz, seqlen, self.n_local_heads, self.head_dim) # 对Query和Key应用旋转嵌入
xq, xk = apply_rotary_emb(xq, xk, freqs_cis=freqs_cis) # 将缓存键和值转换为xq的设备类型 self.cache_k = self.cache_k.to(xq) self.cache_v = self.cache_v.to(xq) # 更新缓存键和值 self.cache_k[:bsz, start_pos : start_pos + seqlen] = xk self.cache_v[:bsz, start_pos : start_pos + seqlen] = xv # 获取键和值 keys = self.cache_k[:bsz, : start_pos + seqlen] values = self.cache_v[:bsz, : start_pos + seqlen] # 转置xq、键和值的维度 xq = xq.transpose(1, 2) keys = keys.transpose(1, 2) values = values.transpose(1, 2) # 计算注意力分数 scores = torch.matmul(xq, keys.transpose(2, 3)) / math.sqrt(self.head_dim) if mask is not None: scores = scores + mask# (bs, n_local_heads, slen, cache_len + slen) scores = F.softmax(scores.float(), dim=-1).type_as(xq) # 使用注意力分数加权求和得到输出 output = torch.matmul(scores, values)# (bs, n_local_heads, slen, head_dim) output = output.transpose( 1, 2 ).contiguous().view(bsz, seqlen, -1) # 应用输出投影 return self.wo(output)
1.3.2 前馈网络FFN:基于SwiGLU
然后是前馈网络FFN部分,需要注意的点就是采用的激活函数,以及激活函数的位置
import torch.nn as nn
import torch.nn.functional as F
class FeedForward(nn.Module):
def __init__(
self,
dim: int,
hidden_dim: int,
multiple_of: int,
):
super().__init__()
# 初始化隐藏层的维度为输入维度的2/3
hidden_dim = int(2 * hidden_dim / 3)
# 调整隐藏层维度为multiple_of的倍数
hidden_dim = multiple_of * ((hidden_dim + multiple_of - 1) // multiple_of)
# 第一个线性层
self.w1 = ColumnParallelLinear(
dim, hidden_dim, bias=False, gather_output=False, init_method=lambda x: x
)
# 第二个线性层
self.w2 = RowParallelLinear(
hidden_dim, dim, bias=False, input_is_parallel=True, init_method=lambda x: x
)
# 第三个线性层
self.w3 = ColumnParallelLinear(
dim, hidden_dim, bias=False, gather_output=False, init_method=lambda x: x
)
def forward(self, x):
# 前向传播函数
return self.w2(F.silu(self.w1(x)) * self.w3(x)) 这里与常见模子中的FFN做一下简单的对比
[*]BART中的FFN,用的是fc->act->fc,用了两层全毗连
[*]GPT中的FFN,用的是conv1D->act->conv1D,也是只用了两层
[*]而LLaMA中的FFN采用了三个全毗连层以实现FFN SwiGLU,即
https://latex.csdn.net/eq?FFN_%7BswiGLU%7D%28x%2C%20W%2C%20V%2C%20W_2%29%20%3D%20%28Swish_1%28xW%29%5Cotimes%20xV%29W_2
1.3.3 Transformer Block:将SA和FFN这两部分拼在一起
然后将SA和FFN这两部分拼在一起就是一个transformer block
import torch
import torch.nn as nn
from typing import Optional
class TransformerBlock(nn.Module):
def __init__(self, layer_id: int, args: ModelArgs):
super().__init__()
# 初始化参数
self.n_heads = args.n_heads # 注意力头的数量
self.dim = args.dim # 模型维度
self.head_dim = args.dim // args.n_heads# 每个注意力头的维度
self.attention = Attention(args) # 注意力机制模块
self.feed_forward = FeedForward(
dim=args.dim, hidden_dim=4 * args.dim, multiple_of=args.multiple_of
)# 前馈神经网络模块
self.layer_id = layer_id# 当前层的ID
self.attention_norm = RMSNorm(args.dim, eps=args.norm_eps)# 注意力模块的归一化
self.ffn_norm = RMSNorm(args.dim, eps=args.norm_eps) # 前馈神经网络模块的归一化
def forward(self, x: torch.Tensor, start_pos: int, freqs_cis: torch.Tensor, mask: Optional):
# 输入x经过self-attention之后,做Add&Norm
h = x + self.attention.forward(self.attention_norm(x), start_pos, freqs_cis, mask)
# 上一步的输出h作为输入,经过前馈神经网络Feed forward之后,做Add&Norm
out = h + self.feed_forward.forward(self.ffn_norm(h))
return out 最后使用torch的module list将transformer block举行堆叠,拼上最前头的embedding部分,就是一个完整的transformer decoder结构了
import torch
import torch.nn as nn
from typing import Optional
class Transformer(nn.Module):
def __init__(self, params: ModelArgs):
super().__init__()
# 初始化参数
self.params = params
self.vocab_size = params.vocab_size# 词汇表大小
self.n_layers = params.n_layers# Transformer模型的层数
# 词嵌入层
self.tok_embeddings = ParallelEmbedding(
params.vocab_size, params.dim, init_method=lambda x: x
)
# Transformer的各个层
self.layers = torch.nn.ModuleList()
for layer_id in range(params.n_layers):
self.layers.append(TransformerBlock(layer_id, params))
# 归一化层
self.norm = RMSNorm(params.dim, eps=params.norm_eps)
# 输出层
self.output = ColumnParallelLinear(
params.dim, params.vocab_size, bias=False, init_method=lambda x: x
)
# 预计算的频率矩阵
self.freqs_cis = precompute_freqs_cis(
self.params.dim // self.params.n_heads, self.params.max_seq_len * 2
)
@torch.inference_mode()
def forward(self, tokens: torch.Tensor, start_pos: int):
_bsz, seqlen = tokens.shape
# Token嵌入和位置编码
h = self.tok_embeddings(tokens)
self.freqs_cis = self.freqs_cis.to(h.device)
freqs_cis = self.freqs_cis
# 生成上三角的mask矩阵(为decoder模型防止标签泄漏)
mask = None
if seqlen > 1:
mask = torch.full((1, 1, seqlen, seqlen), float("-inf"), device=tokens.device)
mask = torch.triu(mask, diagonal=start_pos + 1).type_as(h)
# 逐层计算Transformer
for layer in self.layers:
h = layer(h, start_pos, freqs_cis, mask)
h = self.norm(h)
output = self.output(h[:, -1, :])# 只计算最后一个位置的logits
return output.float() 1.3.4 Transformer的天生过程——预测下一个token
接着看下天生过程,如下:
[*]对prompts举行tokenize,得到token ids; class LLaMA:
def __init__(self, model: Transformer, tokenizer: Tokenizer):
self.model = model
self.tokenizer = tokenizer
def generate(
self,
prompts: List,
max_gen_len: int,
temperature: float = 0.8,
top_p: float = 0.95,
) -> List:
# 获取批处理大小
bsz = len(prompts)
# 获取模型参数
params = self.model.params
# 检查批处理大小是否在允许的最大批处理大小范围内
assert bsz <= params.max_batch_size, (bsz, params.max_batch_size)
# 使用分词器对提示进行编码为token
prompt_tokens =
[*]计算当前batch的最大长度total_len,用来创建输入的token tensor,最大长度不能超过前文所述缓存的大小; # 查找提示token的最小和最大大小
min_prompt_size = min()
max_prompt_size = max()
# 计算要生成的token的总长度
total_len = min(params.max_seq_len, max_gen_len + max_prompt_size)
[*]从当前batch中,最短的一个prompt的位置,作为天生的开始位置,开始天生; # 创建一个张量来存储生成的token,填充为填充token
tokens = torch.full((bsz, total_len), self.tokenizer.pad_id).cuda().long()
# 将提示token复制到token张量中
for k, t in enumerate(prompt_tokens):
tokens = torch.tensor(t).long()
# 创建一个掩码以识别输入文本
input_text_mask = tokens != self.tokenizer.pad_id
# 设置生成的起始位置
start_pos = min_prompt_size
prev_pos = 0
[*]输入的token tensor传入transformer模子,计算logits,得到形状为(batch_size, hidden_size)的logits(transformer最后一层的输出); # 逐个生成token
for cur_pos in range(start_pos, total_len):
# 通过模型进行前向传递以获取logits
logits = self.model.forward(tokens[:, prev_pos:cur_pos], prev_pos)
[*]softmax+top_p采样,得到当前预测的token,并更新当前位置,准备预测下一个token; if temperature > 0:
# 对logits应用温度并计算概率
probs = torch.softmax(logits / temperature, dim=-1)
# 使用top-p采样抽样下一个token
next_token = sample_top_p(probs, top_p)
else:
# 选择概率最高的标记
next_token = torch.argmax(logits, dim=-1)
next_token = next_token.reshape(-1)
# 只有在已经生成了prompt的情况下才替换token
next_token = torch.where(
input_text_mask[:, cur_pos], tokens[:, cur_pos], next_token
)
tokens[:, cur_pos] = next_token
prev_pos = cur_pos
[*]解码得到天生的文本 # 将生成的token解码为文本
decoded = []
for i, t in enumerate(tokens.tolist()):
# 将token截断到最大生成长度
t = t[: len(prompt_tokens) + max_gen_len]
# 将token截断到如果存在结束标记
try:
t = t[: t.index(self.tokenizer.eos_id)]
except ValueError:
pass
# 将标记解码为文本
decoded.append(self.tokenizer.decode(t))
return decoded
至于上述其中的sample_top_p,如下实现
def sample_top_p(probs, p):
# 按降序对概率进行排序
probs_sort, probs_idx = torch.sort(probs, dim=-1, descending=True)
# 计算概率的累积和
probs_sum = torch.cumsum(probs_sort, dim=-1)
# 创建一个掩码以过滤累积概率超过p的标记
mask = probs_sum - probs_sort > p
# 将被过滤的标记的概率设置为0
probs_sort = 0.0
# 归一化概率
probs_sort.div_(probs_sort.sum(dim=-1, keepdim=True))
# 使用修改后的概率进行抽样下一个标记
next_token = torch.multinomial(probs_sort, num_samples=1)
# 收集抽样标记的原始索引
next_token = torch.gather(probs_idx, -1, next_token)
return next_token1.4 LLaMA的Optimizer设计、模子加速优化与微型版本
在Optimizer设计上
[*]该模子使用AdamW优化器(Loshchilov和Hutter,2017)举行训练,超参数设置为β1=0.9,β2=0.95
此外,使用余弦学习率方式,使最终学习率等于最大学习率的10%,以及使用0.1的权重衰减和1.0的梯度剪裁,和2000个warm up计谋,使得可以根据模子的大小改变学习率和批次大小
在模子的加速优化方面
[*]首先,使用一个高效的因果多头注意力方式的实现,灵感来自Rabe和Staats(2021)以及Dao等人(2022),这个实现可在xformers库中找到,可以有效减少内存的使用和计算
具体原理为通过不存储注意力权重和不计算由于语言建模任务的因果性质而被掩饰的键/查询分数来实现的
[*]其次,为了进一步提高训练服从,减少了在check point的后向传递中重新计算的激活量,在实现上,通过手动实现trasnformer层的后向函数来举行操纵
为了充实受益于这种优化,还通过如Korthikanti等人(2022)中采用的方法,举行使用模子和序列并行来减少模子的内存使用
[*]最后,该工作还尽大概地重叠激活的计算和GPU之间在网络上的通信
最终的优化性能效果为:当训练一个65B参数的模子时,代码在2048A100的GPU上处理大约380个token/秒/GPU,并泯灭80GB的内存,这意味着对包含1.4Ttoken的数据集举行训练大约花费了21天
LLaMA发布不久后,一些研究者基于它做了不少工作
[*]一开始最小参数7B的模子也需要近30GB的GPU才能运行,但通过比特和字节库举行浮点优化,能够让模子在单个NVIDIA RTX 3060(显存一样寻常12G)上运行
[*]之后,GitHub 上的一名研究人员甚至能够在Ryzen 7900X CPU上运行LLM的7B 版本,每秒能推断出几个单词
[*]再之后,有研究者推出了llama.cpp,无需 GPU,就能运行 LLaMA
llama.cpp 项目实现了在MacBook上运行 LLaMA,还有开发者成功的在 4GB RAM 的树莓派上运行了 LLaMA 7B
第二部分 各种微调LLaMA:Alpaca(self-instruct)、Vicuna(shareGPT)、BELLE(self-instruct)
2.1 Stanford Alpaca:结合英文语料通过Self Instruct方式微调LLaMA 7B
2.1.1 Stanford Alpaca简介:论文、代码、数据
3月中旬,斯坦福的Rohan Taori等人发布Alpaca(中文名:羊驼):号称只花100美元,大家都可微调Meta家70亿参数的LLaMA大模子(即LLaMA 7B),具体做法是通过52k指令数据,然后在8个80GB A100上训练3个小时,使得Alpaca版的LLaMA 7B在单纯对话上的性能比肩GPT-3.5(text-davinci-003),这便是指令调优LLaMA的意义所在
[*]论文《Alpaca: A Strong Open-Source Instruction-Following Model》
[*]GitHub所在:https://github.com/tatsu-lab/stanford_alpaca
[*]数据所在 (即斯坦福团队微调LLaMA 7B所用的52K英文指令数据):https://raw.githubusercontent.com/tatsu-lab/stanford_alpaca/main/alpaca_data.json
故意思的是,厥后不断有人把这52K的英文指令数据翻译了下,比如:
单纯翻译的斯坦福52K中文指令数据
斯坦福52K中文指令数据(语句上做了中文表达风格的意译)
这52K数据所对应的alpaca_data.json文件是一个字典列表,每个字典包含以下字段:
[*]instruction: str,描述了模子应该执行的任务,52K 条指令中的每一条都是唯一的
[*]input: str,要么是上下文,要么直接输入(optional context or input for the task),例如,当指令是“总结以下文章”时,输入就是文章,大约 40% 的示例有输入
[*]output: str,由GPT3.5对应的API即 text-davinci-003天生的指令的答案
2.1.2 什么是self-instruct方式:提示GPT3/GPT3.5/GPT4的API收集数据
而这52K数据是怎么来的呢?实际上,是通过Self-Instruct『Self-Instruct是来自华盛顿大学Yizhong Wang等人于22年12月通过这篇论文《SELF-INSTRUCT: Aligning Language Model with Self Generated Instructions》提出的,这是其论文所在、代码所在』提示GPT3的API拿到的
https://i-blog.csdnimg.cn/blog_migrate/f47c8a351d9b4a8a44944511fcec47e2.png
具体而言,论文中提出
[*]人工设计175个任务,每个任务都有对应的{指令 输入 输出/实例}或{指令 输出/实例},将这175个任务数据作为种子集
比如这是斯坦福Alpaca的175个种子数据:stanford_alpaca/seed_tasks.jsonl at main · tatsu-lab/stanford_alpaca · GitHub
{"id": "seed_task_0", "name": "breakfast_suggestion",
"instruction": "Is there anything I can eat for a breakfast that doesn't include eggs, yet includes protein, and has roughly 700-1000 calories?",
"instances": [{"input": "", "output": "Yes, you can have 1 oatmeal banana protein shake and 4 strips of bacon. The oatmeal banana protein shake may contain 1/2 cup oatmeal, 60 grams whey protein powder, 1/2 medium banana, 1tbsp flaxseed oil and 1/2 cup watter, totalling about 550 calories. The 4 strips of bacon contains about 200 calories."}],
"is_classification": false}
{"id": "seed_task_1", "name": "antonym_relation",
"instruction": "What is the relation between the given pairs?",
"instances": [{"input": "Night : Day :: Right : Left", "output": "The relation between the given pairs is that they are opposites."}],
"is_classification": false}
[*]然后提示模子比如GPT3对应的API即 text-davinci-001 (原论文中没用text-davinci-003,because their newer engines are trained with the latest user data and are likely to already see the SUPERNI evaluation set,但实际应用时比如斯坦福Alpaca指定的GPT3.5的API即 text-davinci-003天生指令,包括很快你将看到,23年4月还有微软的研究者指定GPT4的API天生指令),使用种子集作为上下文示例来天生更多新的指令
[*]对该模子天生的指令判断是否分类任务
[*]使用模子天生实例
[*]对上述模子天生的数据{指令 输入 输出/实例}过滤掉低质量或相似度高的
[*]将经过过滤和后处理的数据添加到种子池中
一直重复上述2-6步直到种子池有足够多的数据
2.1.3 天生微调LLaMA的52K数据的完整代码
斯坦福的Alpaca在实际天生52K数据时,在上节self-instruct方式的底子上,还思量到了多重过滤机制,防止天生过于相似、过长或含有特定关键词的指令,以此保证天生的指令集的质量和多样性,且在每一轮天生指令后,都会生存当前的效果,方便随时跟踪进度,此外,还采用了多历程处理,提高了服从
故最终完整天生52K数据的完整代码如下(来源于:https://github.com/tatsu-lab/stanford_alpaca/blob/main/generate_instruction.py,且为方便理解,给每一行代码都逐行加上了中文解释)
"""
batch_selfinstruct_generate.py
运行:
python -m generate_instruction generate_instruction_following_data \
--output_dir ./ \
--num_instructions_to_generate 10 \
--model_name="text-davinci-003" \
"""
import time # 引入时间模块
import json # 引入json模块
import os # 引入os模块
import random # 引入随机数模块
import re # 引入正则表达式模块
import string # 引入字符串模块
from functools import partial # 引入偏函数模块
from multiprocessing import Pool # 引入多进程模块
import numpy as np# 引入Numpy库
import tqdm# 引入tqdm库,用于进度条显示
from rouge_score import rouge_scorer # 引入rouge评分器,用于文本相似度计算
import utils# 引入自定义的工具模块
import fire# 引入fire库,用于命令行参数解析
# 定义一个将多个提示指令编码成单一字符串的函数
def encode_prompt(prompt_instructions):
prompt = open("./prompt.txt").read() + "\n"# 打开并读取提示文本文件
# 遍历提示指令,将其格式化并附加到提示字符串中
for idx, task_dict in enumerate(prompt_instructions):
(instruction, input, output) = task_dict["instruction"], task_dict["input"], task_dict["output"]
instruction = re.sub(r"\s+", " ", instruction).strip().rstrip(":")# 对指令进行清洗
input = "<noinput>" if input.lower() == "" else input# 若无输入则标注为"<noinput>"
# 格式化并添加指令、输入和输出到提示中
prompt += f"###\n"
prompt += f"{idx + 1}. Instruction: {instruction}\n"
prompt += f"{idx + 1}. Input:\n{input}\n"
prompt += f"{idx + 1}. Output:\n{output}\n"
prompt += f"###\n"
prompt += f"{idx + 2}. Instruction:"# 添加下一个指令的前缀
return prompt# 返回提示字符串
# 定义一个对GPT-3响应进行后处理的函数,抽取生成的新指令
def post_process_gpt3_response(num_prompt_instructions, response):
if response is None: # 如果响应为空,则返回空列表
return []
raw_instructions = f"{num_prompt_instructions+1}. Instruction:" + response["text"] # 获取原始的指令文本
raw_instructions = re.split("###", raw_instructions)# 根据"###"切分原始指令
instructions = [] # 初始化指令列表
# 对每个切分出的原始指令进行处理
for idx, inst in enumerate(raw_instructions):
# 如果解码由于长度停止,最后一个示例可能被截断,因此我们丢弃它
if idx == len(raw_instructions) - 1 and response["finish_reason"] == "length":
continue
idx += num_prompt_instructions + 1
# 根据索引和"Instruction", "Input", "Output"关键字进行切分
splitted_data = re.split(f"{idx}\.\s+(Instruction|Input|Output):", inst)
if len(splitted_data) != 7:# 如果切分结果不等于7,则继续下一轮循环
continue
else:
# 提取指令、输入、输出
inst = splitted_data.strip()
input = splitted_data.strip()
input = "" if input.lower() == "<noinput>" else input# 对输入进行处理,如果是"<noinput>",则替换为空字符串
output = splitted_data.strip()
# 过滤掉太短或太长的指令
if len(inst.split()) <= 3 or len(inst.split()) > 150:
continue
# 根据不适合语言模型的关键词进行过滤
blacklist = [
"image",
"images",
"graph",
"graphs",
"picture",
"pictures",
"file",
"files",
"map",
"maps",
"draw",
"plot",
"go to",
"video",
"audio",
"music",
"flowchart",
"diagram",
]
# 如果指令中存在黑名单中的词,则忽略该指令
if any(find_word_in_string(word, inst) for word in blacklist):
continue
# 模型倾向于为一些现有指令添加"编写程序",这会导致很多这样的指令。
# 这里过滤掉这类指令
if inst.startswith("Write a program"):
continue
# 过滤那些以标点符号开始的指令
if inst in string.punctuation:
continue
# 过滤那些以非英语字符开始的指令
if not inst.isascii():
continue
# 将处理后的指令添加到指令列表中
instructions.append({"instruction": inst, "input": input, "output": output})
return instructions# 返回指令列表
# 定义一个在字符串中查找单词的函数
def find_word_in_string(w, s):
return re.compile(r"\b({0})\b".format(w), flags=re.IGNORECASE).search(s)
# 定义一个生成指令的函数
def generate_instruction_following_data(
output_dir="./",
seed_tasks_path="./seed_tasks.jsonl",
num_instructions_to_generate=100,
model_name="text-davinci-003",
num_prompt_instructions=3,
request_batch_size=5,
temperature=1.0,
top_p=1.0,
num_cpus=16,
):
seed_tasks = # 读取并解析种子任务
# 从种子任务中提取指令、输入和输出
seed_instruction_data = [
{"instruction": t["instruction"], "input": t["instances"]["input"], "output": t["instances"]["output"]}
for t in seed_tasks
]
print(f"Loaded {len(seed_instruction_data)} human-written seed instructions")# 打印加载的人工编写的种子指令的数量
os.makedirs(output_dir, exist_ok=True)# 创建输出目录
request_idx = 0
# 加载LM生成的指令
machine_instruction_data = []
if os.path.exists(os.path.join(output_dir, "regen.json")):
machine_instruction_data = utils.jload(os.path.join(output_dir, "regen.json"))
print(f"Loaded {len(machine_instruction_data)} machine-generated instructions")# 打印加载的机器生成的指令的数量
# 初始化Rouge得分计算器
scorer = rouge_scorer.RougeScorer(["rougeL"], use_stemmer=False)
# 进度条,总数为要生成的指令数量
progress_bar = tqdm.tqdm(total=num_instructions_to_generate)
if machine_instruction_data:
progress_bar.update(len(machine_instruction_data))# 如果已有机器生成的指令,则更新进度条
# 首先,我们对所有的种子指令和生成的机器指令进行标记
all_instructions = for d in seed_instruction_data] + [
d["instruction"] for d in machine_instruction_data
]
all_instruction_tokens =
# 当机器指令数据的数量小于需要生成的指令数量时,持续生成
while len(machine_instruction_data) < num_instructions_to_generate:
request_idx += 1# 请求索引增加
batch_inputs = []
for _ in range(request_batch_size):
# 只从种子任务中采样
prompt_instructions = random.sample(seed_instruction_data, num_prompt_instructions)
# 将多个提示指令编码成一个字符串
prompt = encode_prompt(prompt_instructions)
batch_inputs.append(prompt)# 将编码的指令添加到批输入列表中
decoding_args = utils.OpenAIDecodingArguments(
temperature=temperature,
n=1,
max_tokens=3072,# 硬编码以最大化长度。请求将自动调整
top_p=top_p,
stop=["\n20", "20.", "20."],# 当出现这些字符串时,生成停止
)
# 记录请求开始的时间
request_start = time.time()
# 调用OpenAI API进行批量生成
results = utils.openai_completion(
prompts=batch_inputs,
model_name=model_name,
batch_size=request_batch_size,
decoding_args=decoding_args,
logit_bias={"50256": -100},# 阻止特定token被生成
)
request_duration = time.time() - request_start# 计算请求的时间
# 开始后处理生成的结果
process_start = time.time()
instruction_data = []
for result in results:
# 对每个结果进行后处理,并获取新的指令
new_instructions = post_process_gpt3_response(num_prompt_instructions, result)
instruction_data.extend(new_instructions)
process_duration = time.time() - process_start# 计算后处理的时间
# 更新进度条
progress_bar.update(len(instruction_data))
print(
f"\nRequest {request_idx} took {request_duration:.2f} seconds, post-processing took {process_duration:.2f} seconds"
)
# 对每一条新指令进行处理
for data in instruction_data:
inst = data["instruction"]
# 使用Rouge得分器对指令进行标记
inst_tokens = scorer._tokenizer.tokenize(inst)
# 计算新指令与已有指令的最大RougeL得分
max_rougeL = max(
.fmeasure for old_inst_tokens in all_instruction_tokens]
)
# 如果RougeL得分大于0.5,则认为该指令与已有指令过于相似,不予采纳
if max_rougeL > 0.5:
continue
# 将新指令添加到已有指令列表和已有指令标记列表中
all_instructions.append(inst)
all_instruction_tokens.append(inst_tokens)
# 将新指令添加到机器生成的指令数据中
machine_instruction_data.append(data)
# 将机器生成的指令数据保存到文件中
utils.jdump(machine_instruction_data, os.path.join(output_dir, "regen.json"))
progress_bar.close()# 关闭进度条
print(f"Generated {len(machine_instruction_data)} instructions")# 打印生成的指令数量
# 随机化并截取生成的指令数据
random.shuffle(machine_instruction_data)
machine_instruction_data = machine_instruction_data[:num_instructions_to_generate]
# 将指令数据转化为任务格式
machine_tasks = []
for data in machine_instruction_data:
task = {
"id": utils.random_id(),
"input": data["input"],
"output": data["output"],
"rating": np.random.uniform(1, 5),# 给指令一个随机的评分,代表指令的质量
"instruction": data["instruction"],
}
machine_tasks.append(task)
# 保存机器生成的任务到文件中
utils.jdump(machine_tasks, os.path.join(output_dir, "regen_tasks.json"))
# 使用fire库解析命令行参数,并调用函数
if __name__ == "__main__":
fire.Fire(generate_instruction_following_data) 所以Alpaca,就是花了不到500美元使用OpenAI API天生了5.2万个这样的示例微调LLaMA搞出来的,个人以为可以取名为 instructLLaMA-7B,^_^
https://i-blog.csdnimg.cn/blog_migrate/c7e5fe17a2428653475ac722933bfe02.png
2.1.4 微软研究者提示GPT4的API天生指令数据
值得一提的是,厥后23年4月有微软的研究者提示GPT4的API举行指令微调「论文所在:INSTRUCTION TUNING WITH GPT-4、GitHub所在:instruction-Tuning-with-GPT-4、项目所在:使用GPT4举行指令调优」,从而天生以下数据
[*]English Instruction-Following Data,generated by GPT-4 using Alpaca prompts
这部分数据在项目文件 alpaca_gpt4_data.json 里,contains 52K instruction-following data generated by GPT-4 with prompts in Alpaca. This JSON file has the same format as Alpaca data, except the output is generated by GPT-4:
instruction: str, describes the task the model should perform. Each of the 52K instructions is unique.
input: str, optional context or input for the task.
output: str, the answer to the instruction as generated by GPT-4.
[*]Chinese Instruction-Following Data,即上面英文数据的中文翻译,存储在项目文件alpaca_gpt4_data_zh.json 里
[*]Comparison Data ranked by GPT-4,好训练一个奖励模子
存储在 comparision_data.json 文件里,ranked responses from three models, including GPT-4, GPT-3.5 and OPT-IML by asking GPT-4 to rate the quality.
user_input: str, prompts used for quering LLMs.
completion_a: str, a model completion which is ranked higher than completion_b.
completion_b: str, a different model completion which has a lower quality score.
[*]Answers on Unnatural Instructions Data,该数据用于大规模量化 GPT-4 与我们的指令调整模子(即LLaMA by instruction tuning with GPT4)之间的差距,而缩小与GPT4的差距便是本次指令调优的目标 https://i-blog.csdnimg.cn/blog_migrate/08f2394a78f460860c72226c4a1f68c9.png
2.1.5 手把手实战:Self-Instruct: Aligning LM with Self Generated Instructions
之前已说过,Self-Instruct是来自华盛顿大学Yizhong Wang等人于22年12月通过这篇论文《SELF-INSTRUCT: Aligning Language Model with Self Generated Instructions》提出的,这是其论文所在、代码所在
为进一步理解self-instruct这个方式的原理与实现细节,我司杜老师把这个self-instruct的方式天生语料的过程实践了下(这是该教程所在),具体而言,先理清如下4个步骤
[*]Step1:通过模子天生新的指令
根据人工设计的175个任务,每个任务都有对应的(指令,输入,输出)或(指令,输出);使用模子天生新的指令;
执行的代码文件为 # 1. Generate instructions from the seed tasks
./scripts/generate_instructions.sh
[*]Step2:对模子天生的指令举行判断(指令是否是一个分类任务)
而判断指令是否属于分类任务的操纵如下:在种子池中随机挑选12条分类指令和19条非分类指令,然后加上新天生的指令
执行的代码文件为 # 2. Identify whether the instruction represents a classification task or not
./scripts/is_clf_or_not.sh
[*]Step3:根据Step2的判断效果,给出差别的输出
如果是分类任务,就通过模子输出 Class_label 和 Input(Output-first,即先输出分类的标签,再输出Input内容)
如果不是分类任务,就通过模子输出 Input 和 Output(Input-first,即先输出Input,再输出Output)
执行的代码文件为 # 3. Generate instances for each instruction
./scripts/generate_instances.sh
[*]Step4:过滤及后处理
对上述模子天生的数据举行过滤和后处理,将经过过滤和后处理的数据添加到种子池中
且为了数据的多样性,新天生的指令只有与种子池中的指令的 ROUGE-L 小于0.7时才会添加进入种子池;
排除一些无法被语言模子处理的指令,比如涉及图像、图片、图形的指令;
在给指令天生实例时,会过滤掉输入相同但是输出差别的实例
执行的代码文件为 # 4. Filtering, processing, and reformatting
./scripts/prepare_for_finetuning.sh
对于以上4个步骤举行不断循环,直到种子池有足够多的数据(通常会设定一个具体的参数,比如:52000),天生过程制止
接下来,我们逐一写代码实现
正式编码之前的一些准备工作
1、首先将代码下载到本地,下面两种方式均可
[*]使用 Download 下载zip文件
[*]git clone https://github.com/yizhongw/self-instruct.git
// 因在windows上操纵的,所以无法执行bash下令,故直接用python下令运行
2、进入conda情况(用的pytorch这个情况) ,安装相关的包
cd self-instruct-main
pip install -r requirements.txt
2.2 Stanford Alpaca的微调拆解——见证LLM微调的一样寻常模式
2.2.1 stanford_alpaca/train.py:微调代码的逐行分析
大概有读者疑问,那微调的代码长啥样呢?实际上,微调步骤大同小异,具体而言,一样寻常直接用的Hugging Face的transformer标准库中的微调代码 (We fine-tune our models using standard Hugging Face training code)
https://i-blog.csdnimg.cn/blog_migrate/19f17afe1953f692b9ad44960d9e3b3b.png
[*]首先安装pip install -r requirements.txt
[*]下面的下令在具有 4 个 A100 80G GPU 且处于 FSDP 模式的呆板上使用52K的数据集对 LLaMA-7B 举行微调
full_shard
替换<your_random_port>为你自己的端口
<your_path_to_hf_converted_llama_ckpt_and_tokenizer>转换后的查抄点和分词器的路径
以及<your_output_dir>你想要存储输出的位置 {
"id": "seed_task_0",
"name": "breakfast_suggestion",
"instruction": "Is there anything I can eat for a breakfast that doesn't include eggs,
yet includes protein, and has roughly 700-1000 calories?",
"instances": [{"input": "", "output": "Yes, you can have 1 oatmeal banana protein shake and 4 strips of bacon.
The oatmeal banana protein shake may contain 1/2 cup oatmeal, 60 grams whey protein powder,
1/2 medium banana, 1tbsp flaxseed oil and 1/2 cup watter, totalling about 550 calories. The 4 strips of bacon contains about 200 calories."}],
"is_classification": false
}
[*]当然,以上脚本也实用于对 OPT 微调,这是微调 OPT-6.7B 的示例 python self_instruct/bootstrap_instructions.py --batch_dir data/ceshi --num_instructions_to_generate 100 --seed_tasks_path data/seed_tasks.jsonl --engine "davinci" --api_key "自己的openai API"
好,现在题目来了,上面脚本中的微调代码即train.py到底长啥样呢?
据Stanford Alpaca于Mar 17, 2023发布的231行的训练代码stanford_alpaca/train.py at aa65c492bb788e144712daab42bc5d11c2761591 · tatsu-lab/stanford_alpaca · GitHub (注:后代码于Apr 16, 2023有更新,代码略微缩小至222行代码,详见:https://github.com/tatsu-lab/stanford_alpaca/blob/main/train.py),可得微调的步骤如下
[*]导入所需的库:包括torch,transformers等 fout.write(json.dumps({
"instruction": inst,
"most_similar": most_similar_instructions,
"avg_similarity_score": float(np.mean(rouge_scores)),
"metadata": metadata,
"request_idx": request_idx
}) + "\n")
[*]定义一些全局变量,如特别字符、提示模板等
[*]定义用于处理模子、数据和训练参数的数据类 parser.add_argument(
"--num_prompt_instructions",
type=int,
default=8,
help="The number of instructions to use in the prompt."
)
[*]定义辅助函数,如:
[*]safe_save_model_for_hf_trainer :安全地生存训练器中的模子 # load the LM-generated instructions,使用生成模型得到新的100条 instruction 提示
machine_instructions = []
# 开始生成100条instruction提示数据
# 使用文件操作打开一个文件,该文件位于指定的批处理目录中
# 文件名为"machine_generated_instructions.jsonl",以追加模式打开,然后把文件对象赋值给fout
with open(os.path.join(args.batch_dir, "machine_generated_instructions.jsonl"), "a") as fout:
# 进入循环,当生成模型产生的指令数量未达到用户指定的数量时,继续产生新的指令
while len(machine_instructions) < args.num_instructions_to_generate:
# 初始化一个列表,用于保存批处理的输入数据
batch_inputs = []
# args.request_batch_size为5
# 循环指定的批处理大小的次数,每次循环都会产生一条新的指令
for _ in range(args.request_batch_size):
# 调用函数从生成模型中抽样生成指令,这里选择的指令数量为2,然后将生成的指令保存到变量prompt_instructions
prompt_instructions = sample_machine_instructions(
machine_instructions,
similarities=None,
n=2)
'''
sample human instructions from the pool
从默认的175条中选再选6条seed_instructions,加上上面使用LLM最初生成的2条prompt_instructions,相当于一共选了8条
(最开始的时候,machine_instructions为空,因此会直接从175条中直接选8条)
'''
prompt_instructions += random.sample(seed_instructions, args.num_prompt_instructions - len(prompt_instructions))
# 对这8条指令进行随机排序
random.shuffle(prompt_instructions)
# 将这8条指令编码成模型可以接收的输入格式,然后保存到变量prompt
prompt = encode_prompt(prompt_instructions, classification=args.use_clf_seed_tasks_only)
# 将编码后的输入添加到批处理的输入数据列表中
batch_inputs.append(prompt)
# 调用函数使用GPT-3引擎对批处理的输入数据进行处理,处理的参数包括最大的输出词汇数量、输出的随机性、输出结果的顶部概率等
results = make_gpt3_requests(
engine=args.engine,
prompts=batch_inputs,
max_tokens=1024,
temperature=0.7,
top_p=0.5,
frequency_penalty=0,
presence_penalty=2,
stop_sequences=["\n\n", "\n16", "16.", "16 ."],
logprobs=1,
n=1,
best_of=1,
api_key=args.api_key,
organization=args.organization,
)
[*]smart_tokenizer_and_embedding_resize :调整分词器和词嵌入大小 # 构建prompt数据,针对是否分类分别构建不同的prompt数据
# 定义一个函数,该函数用于将多个提示指令编码成一个字符串
# 该函数接受两个参数,第一个参数是提示指令列表,第二个参数表示是否是分类任务,是=>输出优先,否=>输入优先,对应的 prompt_instructions/prompt_instances 不一样
def encode_prompt(prompt_instructions, classification=False):
"""Encode multiple prompt instructions into a single string."""
# 如果当前任务是分类任务,那么设置提示信息为一个固定的字符串
if classification:
# 这个提示信息是引导用户生成一系列的分类任务,如果可能的话,要求用户明确指定可能的输出标签
prompt = "Referring to a series of classification tasks, generate 8 more new tasks. Try to specify the possible output labels when possible.\n"
# 如果当前任务不是分类任务,那么设置提示信息为另一个固定的字符串
else:
# 这个提示信息是引导用户生成一系列的任务
prompt = "Referring to these eight tasks, generate 8 more new tasks:\n"
# 循环处理每一条提示指令
for idx, instruction in enumerate(prompt_instructions):
# 使用正则表达式将指令中的多余空格替换为单个空格,并去掉前后的空格以及末尾的冒号
instruction = re.sub(r"\s+", " ", instruction).strip().rstrip(":")
# 将处理后的指令添加到提示信息中,注意指令前面需要添加序号
prompt += f"{idx+1}. {instruction}\n"
# 在所有指令之后添加一个空白的序号,这个序号是接下来用户需要填写的新任务的序号
prompt += f"{len(prompt_instructions) + 1}."
# 返回编码后的提示信息
return prompt
[*]_tokenize_fn :将字符串序列举行分词 python self_instruct/identify_clf_or_not.py --batch_dir data/ceshi --engine "davinci" --request_batch_size 5 --api_key "自己的openai API"
[*]preprocess :预处理数据,对源数据和目标数据举行分词 {"instruction": "Find the largest number in this list.", "is_classification": " Yes"}
{"instruction": "What is the first name of your favorite actor?", "is_classification": " No"}
{"instruction": "Give me the number of distinct elements in this set.", "is_classification": " Yes"}
{"instruction": "Give me the top 5 countries that are exporting tea.", "is_classification": " Yes"}
[*]定义SupervisedDataset 类(用于监督微调的数据集),用于加载数据、格式化输入、举行分词等操纵 # 执行输出过程
# 使用文件操作打开一个输出文件,然后把文件对象赋值给fout
with open(output_path, "w") as fout:
# 迭代输入的数据行,步长为request_batch_size
for batch_idx in range(0, len(lines), args.request_batch_size):
# 对每个批次,将批次中的数据行转换为JSON对象
batch = ]
# 检查批次中的所有指令是否都在已存在的请求中
if all(d["instruction"] in existing_requests for d in batch):
# 如果都在,则直接从已存在的请求中获取数据,并写入到输出文件中
for d in batch:
data = existing_requests]
data = OrderedDict(
(k, data) for k in \
["instruction", "is_classification"]
)
fout.write(json.dumps(data, ensure_ascii=False) + "\n")
else:
# 如果不都在,那么需要使用GPT-3引擎生成数据
# 首先构造一个提示,这个提示包含前缀和指令
# prefix = compose_prompt_prefix(human_written_tasks, batch["instruction"], 8, 2)
prefix = templates
prompts = .strip() + "\n" + "Is it classification?" for d in batch]
# 调用函数使用GPT-3引擎对批处理的输入数据进行处理
# 处理的参数包括最大的输出词汇数量、输出的随机性、输出结果的顶部概率等
results = make_gpt3_requests(
engine=args.engine,
prompts=prompts,
max_tokens=3,
temperature=0,
top_p=0,
frequency_penalty=0,
presence_penalty=0,
stop_sequences=["\n", "Task"],
logprobs=1,
n=1,
best_of=1,
api_key=args.api_key,
organization=args.organization)
# 将结果写入到输出文件中
for i in range(len(batch)):
data = batch
# 如果结果存在,则将结果中的"is_classification"字段保存到数据中
if results["response"] is not None:
data["is_classification"] = results["response"]["choices"]["text"]
else:
# 如果结果不存在,则将"is_classification"字段设置为空
data["is_classification"] = ""
# 构造一个字典,包含指令和"is_classification"字段
data = {
"instruction": data["instruction"],
"is_classification": data["is_classification"]
}
# 对字典进行排序,然后将字典转换为JSON字符串,并写入到输出文件中
data = OrderedDict(
(k, data) for k in \
["instruction", "is_classification"]
)
fout.write(json.dumps(data, ensure_ascii=False) + "\n")
[*]定义DataCollatorForSupervisedDataset 类,用于将数据集的实例整理为批次 python self_instruct/generate_instances.py --batch_dir data/ceshi --input_file machine_generated_instructions.jsonl --output_file machine_generated_instances.jsonl --max_instances_to_gen 5 --engine "davinci" --request_batch_size 5 --api_key "自己的openai API"
[*]定义make_supervised_data_module 函数,用于创建监督学习任务的数据集和整理器 # 使用文件操作打开一个输出文件,以utf-8的编码格式,然后把文件对象赋值给fout
with open(output_path, "w", encoding='utf-8') as fout:
# 迭代任务数据,步长为request_batch_size
for batch_idx in range(0, len(tasks), args.request_batch_size):
# 获取当前批次的任务
batch = tasks
# 检查批次中的所有指令是否都在已存在的请求中
if all(d["instruction"] in existing_requests for d in batch):
# 如果都在,则直接从已存在的请求中获取数据,并写入到输出文件中
for d in batch:
data = existing_requests]
# 只选择关键字段创建有序字典
data = OrderedDict(
(k, data) for k in \
["instruction", "raw_instances", "instance_metadata", "instruction_metadata",
"most_similar", "avg_similarity_score"]
)
# 写入数据到输出文件
fout.write(json.dumps(data, ensure_ascii=False) + "\n")
else:
# 如果不都在,那么需要构建请求的prompts
prompts = []
for task in batch:
# 根据任务的类型,使用不同的模板构建prompt
if task_clf_types]:
prompt = output_first_template_for_clf + " " + task["instruction"].strip() + "\n"
prompts.append(prompt)
else:
prompt = input_first_template_for_gen + " " + task["instruction"].strip() + "\n"
prompts.append(prompt)
# 使用GPT-3引擎发送请求
results = make_gpt3_requests(
engine=args.engine,
prompts=prompts,
# 根据任务类型调整最大token数
max_tokens=300 if any(task_clf_types] for task in batch) else 350,
temperature=0,
top_p=0,
frequency_penalty=0,
presence_penalty=1.5,
stop_sequences=,
logprobs=1,
n=1,
best_of=1,
api_key=args.api_key,
organization=args.organization)
# 将结果写入到输出文件中
for i in range(len(batch)):
data = batch
# 保存请求的元数据
data["instance_metadata"] = results
# 如果结果存在,则保存生成的实例
if results["response"] is not None:
data["raw_instances"] = results["response"]["choices"]["text"]
else:
# 如果结果不存在,则设置为空
data["raw_instances"] = ""
# 构建有序字典
data = OrderedDict(
(k, data) for k in \
["instruction", "raw_instances", "instance_metadata", "instruction_metadata",
"most_similar", "avg_similarity_score"]
)
# 写入数据到输出文件
fout.write(json.dumps(data, ensure_ascii=False) + "\n")
# 更新进度条
progress_bar.update(len(batch))
[*]定义train函数def train() :,用于执行以下操纵:
a. 剖析下令行参数:使用transformers.HfArgumentParser 剖析下令行参数,将它们分为模子参数、数据参数和训练参数 python self_instruct/prepare_for_finetuning.py --instance_files data/ceshi/machine_generated_instances.jsonl --classification_type_files data/ceshi/is_clf_or_not_davinci_template_1.jsonl --output_dir data/ceshi/finetuning_data --include_seed_tasks --seed_tasks_path data/seed_tasks.jsonl b. 加载预训练模子:使用transformers.AutoModelForCausalLM.from_pretrained 从预训练的模子查抄点加载一个用于因果语言建模的模子 # 使用tqdm模块,这是一个快速,可扩展的Python进度条,遍历生成的任务
for task in tqdm.tqdm(generated_tasks):
# 从任务中提取出指令
instruction = task["instruction"]
# 根据指令判断任务是否为分类任务,并存储结果
task["is_classification"] = task_clf_types
# 根据任务类型,解析并获取对应的实例
if task["is_classification"]:
task_instances = parse_instances_for_classification_task(task["raw_instances"], instruction, task["instance_metadata"])
else:
task_instances = parse_instances_for_generation_task(task["raw_instances"], instruction, task["instance_metadata"])
# 每个任务最多取5个实例,如果实例数少于5,则取全部
task_instances = random.sample(task_instances, min(len(task_instances), 5))
# 如果任务没有实例,则跳过当前循环
if not task_instances:
continue
# 将实例添加到训练实例列表中
training_instances += task_instances
# 初始化GPT-3实例列表
gpt3_instances = []
# 遍历训练实例
for instance in training_instances:
# 获取输入
inst_input = instance
# 对输入进行预处理,可能会去除冒号前的部分,或替换连续的两个新行符为一个新行符
if random.random() < 0.5:
colon_words = re.findall(r"(\w+):", inst_input)
if len(set(colon_words)) == 1:
inst_input = inst_input.split(":", 1).strip()
else:
inst_input = inst_input.strip()
inst_input = inst_input.replace("\n\n", "\n")
# 对实例进行编码,并添加到GPT-3实例列表
gpt3_instances.append(encode_instance(instance, inst_input, instance))
# 初始化过滤实例列表和实例集合,用于移除重复实例
filtered_instances = []
prompt_completion_set = set()
# 遍历GPT-3实例
for instance in gpt3_instances:
# 创建实例对
instance_pair = (instance["prompt"], instance["completion"])
# 如果实例对不在集合中,添加到集合和过滤实例列表中
if instance_pair not in prompt_completion_set:
prompt_completion_set.add((instance["prompt"], instance["completion"]))
filtered_instances.append(instance)
# 使用过滤后的实例替换原来的GPT-3实例
gpt3_instances = filtered_instances
# 打乱GPT-3实例顺序
random.shuffle(gpt3_instances)
# 打开文件,准备将GPT-3实例写入文件
with open(os.path.join(args.output_dir, f"gpt3_finetuning_data_{len(gpt3_instances)}.jsonl"), "w") as fout:
# 遍历GPT-3实例
for instance in gpt3_instances:
# 将实例转化为json格式并写入文件
fout.write(json.dumps({
"prompt": instance["prompt"],
"completion": instance["completion"],
}) + "\n") c. 加载分词器:使用transformers.AutoTokenizer.from_pretrained 从预训练的模子查抄点加载分词器 torchrun --nproc_per_node=4 --master_port=<your_random_port> train.py \
--model_name_or_path <your_path_to_hf_converted_llama_ckpt_and_tokenizer> \
--data_path ./alpaca_data.json \
--bf16 True \
--output_dir <your_output_dir> \
--num_train_epochs 3 \
--per_device_train_batch_size 4 \
--per_device_eval_batch_size 4 \
--gradient_accumulation_steps 8 \
--evaluation_strategy "no" \
--save_strategy "steps" \
--save_steps 2000 \
--save_total_limit 1 \
--learning_rate 2e-5 \
--weight_decay 0. \
--warmup_ratio 0.03 \
--lr_scheduler_type "cosine" \
--logging_steps 1 \
--fsdp "full_shard auto_wrap" \
--fsdp_transformer_layer_cls_to_wrap 'LlamaDecoderLayer' \
--tf32 True d. 为分词器添加特别字符:根据需要,将特别字符添加到分词器中 torchrun --nproc_per_node=4 --master_port=<your_random_port> train.py \
--model_name_or_path "facebook/opt-6.7b" \
--data_path ./alpaca_data.json \
--bf16 True \
--output_dir <your_output_dir> \
--num_train_epochs 3 \
--per_device_train_batch_size 4 \
--per_device_eval_batch_size 4 \
--gradient_accumulation_steps 8 \
--evaluation_strategy "no" \
--save_strategy "steps" \
--save_steps 2000 \
--save_total_limit 1 \
--learning_rate 2e-5 \
--weight_decay 0. \
--warmup_ratio 0.03 \
--lr_scheduler_type "cosine" \
--logging_steps 1 \
--fsdp "full_shard auto_wrap" \
--fsdp_transformer_layer_cls_to_wrap 'OPTDecoderLayer' \
--tf32 True e. 创建数据集和整理器:使用make_supervised_data_module 函数为监督学习任务创建数据集和整理器 import copy
import logging
from dataclasses import dataclass, field
from typing import Optional, Dict, Sequence
import torch
import transformers
from torch.utils.data import Dataset
from transformers import Trainer
import utils f. 实例化Trainer类:实例化transformers.Trainer 类,并传入模子、分词器、训练参数以及数据集。Trainer类负责管理训练过程 # 这是Python中的装饰器,用于指示该类是一个数据类。数据类是一个专门用于存储数据的类
# 它为我们自动实现了一些基础方法,如__init__,__repr__,__eq__等
@dataclass
# 定义一个名为ModelArguments的数据类
class ModelArguments:
# 定义一个名为model_name_or_path的实例变量,类型为Optional,默认值为"facebook/opt-125m"
model_name_or_path: Optional = field(default="facebook/opt-125m")
@dataclass
# 定义一个名为DataArguments的数据类
class DataArguments:
# 定义一个名为data_path的实例变量,类型为str,默认值为None,额外的metadata提供了该变量的帮助信息
data_path: str = field(default=None, metadata={"help": "Path to the training data."})
@dataclass
# 定义一个名为TrainingArguments的数据类,这个类继承了transformers库的TrainingArguments类
class TrainingArguments(transformers.TrainingArguments):
# 定义一个名为cache_dir的实例变量,类型为Optional,默认值为None
cache_dir: Optional = field(default=None)
# 定义一个名为optim的实例变量,类型为str,默认值为"adamw_torch"
optim: str = field(default="adamw_torch")
# 定义一个名为model_max_length的实例变量,类型为int
model_max_length: int = field(
default=512,
metadata={"help": "Maximum sequence length. Sequences will be right padded (and possibly truncated)."},
) g. 训练模子:调用Trainer类的train() 方法对模子举行微调,相当于链路就是:transformers库 https://latex.csdn.net/eq?%5Crightarrow Trainer类 https://latex.csdn.net/eq?%5Crightarrow train函数 def safe_save_model_for_hf_trainer(trainer: transformers.Trainer, output_dir: str):
"""Collects the state dict and dump to disk."""
state_dict = trainer.model.state_dict()
if trainer.args.should_save:
cpu_state_dict = {key: value.cpu() for key, value in state_dict.items()}
del state_dict
trainer._save(output_dir, state_dict=cpu_state_dict)# noqa h. 生存模子状态:在训练完成后,调用Trainer.save_state() 方法生存模子的状态 # 定义一个函数,函数名为 smart_tokenizer_and_embedding_resize,输入包括一个字典(用于定义特殊词汇),一个分词器和一个预训练模型
def smart_tokenizer_and_embedding_resize(
special_tokens_dict: Dict,
tokenizer: transformers.PreTrainedTokenizer,
model: transformers.PreTrainedModel,
):
"""Resize tokenizer and embedding.
Note: This is the unoptimized version that may make your embedding size not be divisible by 64.
"""
# 向分词器添加特殊词汇,返回新添加的词汇数量
num_new_tokens = tokenizer.add_special_tokens(special_tokens_dict)
# 将模型的嵌入层大小调整为与新的词汇表大小一致
model.resize_token_embeddings(len(tokenizer))
# 如果添加了新的词汇
if num_new_tokens > 0:
# 获取模型输入嵌入的权重数据
input_embeddings = model.get_input_embeddings().weight.data
# 获取模型输出嵌入的权重数据
output_embeddings = model.get_output_embeddings().weight.data
# 计算输入嵌入中旧词汇的平均向量
input_embeddings_avg = input_embeddings[:-num_new_tokens].mean(dim=0, keepdim=True)
# 计算输出嵌入中旧词汇的平均向量
output_embeddings_avg = output_embeddings[:-num_new_tokens].mean(dim=0, keepdim=True)
# 将新添加的词汇的输入嵌入向量设置为旧词汇的平均输入嵌入向量
input_embeddings[-num_new_tokens:] = input_embeddings_avg
# 将新添加的词汇的输出嵌入向量设置为旧词汇的平均输出嵌入向量
output_embeddings[-num_new_tokens:] = output_embeddings_avg i. 将训练器的模子安全地生存到磁盘:使用safe_save_model_for_hf_trainer 函数将训练器中的模子安全地生存到磁盘 # 函数定义,接受一个字符串序列和一个预训练的分词器,返回一个字典
def _tokenize_fn(strings: Sequence, tokenizer: transformers.PreTrainedTokenizer) -> Dict:
"""Tokenize a list of strings."""
tokenized_list = [
tokenizer(
text,# 对每个字符串进行分词处理
return_tensors="pt", # 返回PyTorch tensors
padding="longest", # padding策略为 "longest",即填充到最长序列的长度
max_length=tokenizer.model_max_length,# 最大长度为分词器的最大长度
truncation=True, # 如果序列超过最大长度,则进行截断
)
for text in strings # 遍历输入的每个字符串
]
# 从分词结果中提取输入的ids和标签
input_ids = labels = for tokenized in tokenized_list]
# 计算输入ids和标签的长度(不包括padding)
input_ids_lens = labels_lens = [
tokenized.input_ids.ne(tokenizer.pad_token_id).sum().item() for tokenized in tokenized_list
]
# 返回一个字典,包含输入的ids、标签、输入的长度和标签的长度
return dict(
input_ids=input_ids,
labels=labels,
input_ids_lens=input_ids_lens,
labels_lens=labels_lens,
)
[*]如果这个脚本是主程序,则调用train 函数以开始训练过程 # 函数定义,接受源字符串、目标字符串和一个预训练的分词器,返回一个字典
def preprocess(
sources: Sequence,
targets: Sequence,
tokenizer: transformers.PreTrainedTokenizer,
) -> Dict:
# 将源字符串和目标字符串组合在一起
examples =
# 对组合后的字符串和源字符串分别进行分词处理
examples_tokenized, sources_tokenized =
input_ids = examples_tokenized["input_ids"] # 从组合后的分词结果中提取输入ID
labels = copy.deepcopy(input_ids) # 复制一份输入ID作为标签
for label, source_len in zip(labels, sources_tokenized["input_ids_lens"]):
# 对于标签,将源字符串部分的ID设置为忽略索引(IGNORE_INDEX)
label[:source_len] = IGNORE_INDEX
# 返回一个字典,包含输入ID和标签
return dict(input_ids=input_ids, labels=labels)
这份训练/微调代码很经典,包括像此文《从GLM、ChatGLM-6B、MOSS、baichuan-7B到垂类医疗/金融/法律模子、可商用模子》第七部分“各种医疗类ChatGPT:或中英文数据微调LLaMA、或中文数据微调ChatGLM”里的chatdoctor (基于医疗语料微调LLaMA),也用的这份标准代码
这是chatdoctor代码库中的微调代码:https://github.com/Kent0n-Li/ChatDoctor/blob/main/train.py#L192,如出一辙,没有任何差别
https://i-blog.csdnimg.cn/blog_migrate/e39ac29ec0704e97c8d535fb6533d3b8.pnghttps://i-blog.csdnimg.cn/blog_migrate/0c71c134cfa44f7a82e9cdbf070689c2.png2.2.2 Hugging face社区实现的鼎鼎大名的transformers库
但,大概很快便有同砚疑问,怎么没有预想中的损失计算、梯度降落、参数更新呢,实际上这三步的具体实现都封装在了Hugging face社区实现的鼎鼎大名的transformers库的Trainer类中:transformers/trainer.py at main · huggingface/transformers · GitHub
这个 transformers/trainer.py 文件的主要部分如下
https://latex.csdn.net/eq?%5CRightarrow 导入:文件首先导入了一些须要的Python库,如os、sys、logging以及其他一些库。它还导入了Hugging Face库中的一些相关模块,如datasets、transformers等
https://latex.csdn.net/eq?%5CRightarrow TrainerState:这个类用于生存训练器的状态,包括当前的epoch、迭代步数、最佳指标值等
https://latex.csdn.net/eq?%5CRightarrow TrainOutput:这个类用于返回训练过程的效果,包括训练损失、训练步数等
https://latex.csdn.net/eq?%5CRightarrow TrainerControl:这个类提供了一种用于控制训练循环的机制,例如,当用户想要在某个特定的迭代步数时制止训练
https://latex.csdn.net/eq?%5CRightarrow Trainer:这是文件中的主要类,用于训练和评估Transformers模子,它包含许多方法,如train、evaluate、predict等
更具体的,Trainer类包括如下关键方法:
__init__:初始化方法,用于创建训练器对象。它吸取模子、训练参数、数据集等作为输入,并设置相关属性
# 定义一个用于监督学习微调的数据集类
class SupervisedDataset(Dataset):
def __init__(self, data_path: str, tokenizer: transformers.PreTrainedTokenizer):
super(SupervisedDataset, self).__init__() # 初始化父类
logging.warning("Loading data...") # 记录开始加载数据的日志
list_data_dict = utils.jload(data_path) # 加载数据
logging.warning("Formatting inputs...") # 记录开始格式化输入的日志
# 从字典中获取输入提示和无输入提示
prompt_input, prompt_no_input = PROMPT_DICT["prompt_input"], PROMPT_DICT["prompt_no_input"]
sources = [
prompt_input.format_map(example) if example.get("input", "") != "" else prompt_no_input.format_map(example)
# 遍历每个例子,如果有输入则使用输入提示,否则使用无输入提示
for example in list_data_dict
]
# 构造目标,每个目标是输出加上结束标记
targets = }{tokenizer.eos_token}" for example in list_data_dict]
# 记录开始分词输入的日志
logging.warning("Tokenizing inputs... This may take some time...")
data_dict = preprocess(sources, targets, tokenizer)# 预处理源和目标
self.input_ids = data_dict["input_ids"] # 保存输入ID
self.labels = data_dict["labels"] # 保存标签
# 返回数据集的大小
def __len__(self):
return len(self.input_ids)
# 返回第i个样本,包含输入ID和标签
def __getitem__(self, i) -> Dict:
return dict(input_ids=self.input_ids, labels=self.labels)train:这个方法负责整个训练过程,它包括遍历数据集、计算损失、计算梯度、更新模子参数以及日志记录等
[*]遍历数据集:train方法通过使用dataloader来遍历训练数据集 # 定义一个用于监督学习微调的数据整理类
@dataclass
class DataCollatorForSupervisedDataset(object):
# 预训练的分词器
tokenizer: transformers.PreTrainedTokenizer
# 从实例中提取输入ID和标签
def __call__(self, instances: Sequence) -> Dict:
input_ids, labels = tuple( for instance in instances] for key in ("input_ids", "labels"))
# 对输入ID进行填充,使它们具有相同的长度,填充值为分词器的填充标记ID
input_ids = torch.nn.utils.rnn.pad_sequence(
input_ids, batch_first=True, padding_value=self.tokenizer.pad_token_id
)
# 对标签进行填充,使它们具有相同的长度,填充值为忽略索引(IGNORE_INDEX)
labels = torch.nn.utils.rnn.pad_sequence(labels, batch_first=True, padding_value=IGNORE_INDEX)
# 返回一个字典,包含输入ID、标签和注意力掩码。注意力掩码用于指示哪些元素应该被模型关注(在这里是非填充的元素)
return dict(
input_ids=input_ids,
labels=labels,
attention_mask=input_ids.ne(self.tokenizer.pad_token_id),
)
[*]计算损失:损失计算在training_step方法中,吸取输入数据并产生预测输出,然后,这个预测输出会与真实输出(标签)举行比力,以计算损失 # 函数定义,接受一个预训练的分词器和数据参数,返回一个字典
def make_supervised_data_module(tokenizer: transformers.PreTrainedTokenizer, data_args) -> Dict:
"""Make dataset and collator for supervised fine-tuning."""
# 创建一个监督学习的微调数据集
train_dataset = SupervisedDataset(tokenizer=tokenizer, data_path=data_args.data_path)
# 创建一个数据整理器
data_collator = DataCollatorForSupervisedDataset(tokenizer=tokenizer)
# 返回一个字典,包含训练数据集、评估数据集和数据整理器。在这里,评估数据集为None
return dict(train_dataset=train_dataset, eval_dataset=None, data_collator=data_collator) 上述代码行使用model(已经加载了预训练模子)和inputs(包含输入数据的字典)计算模子的预测输出。这个outputs变量包含模子预测的效果
接下来,我们从outputs中获取预测效果,并与真实标签(即labels)举行比力,以计算损失 parser = transformers.HfArgumentParser((ModelArguments, DataArguments, TrainingArguments))
model_args, data_args, training_args = parser.parse_args_into_dataclasses() outputs.loss是模子预测输出和真实输出(标签)之间的损失。这个损失值将用于计算梯度并更新模子参数
[*]计算梯度: model = transformers.AutoModelForCausalLM.from_pretrained(
model_args.model_name_or_path,
cache_dir=training_args.cache_dir,
)这行代码计算模子参数关于损失的梯度 model = transformers.AutoModelForCausalLM.from_pretrained(
model_args.model_name_or_path,
cache_dir=training_args.cache_dir,
) 梯度累积:且当gradient_accumulation_steps大于1时,梯度会被累积,而不是立即更新模子参数 # 从预训练模型创建一个自动化的分词器,其中包含了模型的名称或路径,缓存目录,模型的最大长度,填充的位置以及是否使用快速分词器
tokenizer = transformers.AutoTokenizer.from_pretrained(
model_args.model_name_or_path,
cache_dir=training_args.cache_dir,
model_max_length=training_args.model_max_length,
padding_side="right",
use_fast=False,
)
# 如果分词器没有pad token,那么添加一个,并重新设置模型的嵌入大小
if tokenizer.pad_token is None:
smart_tokenizer_and_embedding_resize(
special_tokens_dict=dict(pad_token=DEFAULT_PAD_TOKEN),
tokenizer=tokenizer,
model=model,
)
[*]更新模子参数:optimizer.step()这行代码根据计算出的梯度来更新模子参数 # 如果模型名包含"llama",则为分词器添加特殊的token,包括eos_token,bos_token以及unk_token
if "llama" in model_args.model_name_or_path:
tokenizer.add_special_tokens(
{
"eos_token": DEFAULT_EOS_TOKEN,
"bos_token": DEFAULT_BOS_TOKEN,
"unk_token": DEFAULT_UNK_TOKEN,
}
) 且慢,暂停解释下为何会有梯度累积这个操纵呢?缘故原由在于batch size越大,局部数据求得的梯度方向越接近全局的梯度优化方向,那怎么增大batch size呢?一方面可以增长硬件资源,二方面可以通过梯度累积
举个例子,假定我们有1000个样本的数据集,我们可以将其分成10个小批次,每个小批次包含100个样本
https://latex.csdn.net/eq?%5Crightarrow 梯度累积:在每个小批次的训练中,我们管帐算出模子参数的梯度,然后将这些梯度累加起来(可以设定一个参数gradient_accumulation_steps,以指定我们想要累积多少个小批次的梯度,比如5),而不是立即用它们来更新模子参数
https://latex.csdn.net/eq?%5Crightarrow 参数更新:当我们处理完gradient_accumulation_steps个小批次后,我们就使用累积的梯度来更新模子的参数
https://latex.csdn.net/eq?%5Crightarrow 梯度清零:在每次参数更新后,我们都会将累积的梯度清零,以便于开始下一个梯度累积和参数更新的周期,最终处理完剩下的5个批次
值得一提的是,通常情况下,我们会举行多个epoch的训练(每次举行新的epoch时,数据打乱),每个epoch后都会对模子的性能举行评估,并根据评估效果来调整学习率等超参数
[*]学习率调整:lr_scheduler.step()根据预定义的学习率调度计谋更新学习率 data_module = make_supervised_data_module(tokenizer=tokenizer, data_args=data_args)
[*]日志记录:log方法用于记录训练过程中的一些关键指标,例如损失、学习率等
evaluate:这个方法用于评估模子在验证数据集上的性能,返回评估效果
trainer = Trainer(model=model, tokenizer=tokenizer, args=training_args, **data_module)predict:这个方法用于在给定的数据集上举行预测,返回预测效果
trainer.train()save_model:这个方法用于将训练好的模子生存到指定的目录
trainer.save_state() https://latex.csdn.net/eq?%5CRightarrow ShardedDDPOption:这是一个可选的类,用于支持使用混淆精度和ZeRO举行分布式训练
2.2.3 Alpaca-LoRA:通过PEFT库在消耗级GPU上微调「基于LLaMA的Alpaca」
在神经网络模子中,模子参数通常以矩阵的情势表现。对于一个预训练好的模子,其参数矩阵已经包含了许多有效的信息。为了使模子顺应特定任务,我们需要对这些参数举行微调
LoRA的焦点思想是用一种低秩的方式来调整这些参数矩阵。在数学上,低秩意味着一个矩阵可以用两个较小的矩阵相乘来近似,通过论文《LORA: LOW-RANK ADAPTATION OF LARGE LANGUAGE MODELS》可知(这是解读之一)
https://i-blog.csdnimg.cn/blog_migrate/5a2f143dcc8cb51d3b36a1541484cfa3.png
[*]选择目标层:首先,在预训练神经网络模子中选择要应用LoRA的目标层。这些层通常是与特定任务相关的,如自注意力机制中的查询Q和键K矩阵
[*]初始化映射矩阵和逆映射矩阵:为目标层创建两个较小的矩阵A和B
https://latex.csdn.net/eq?%5Crightarrow A是映射矩阵(一样寻常用随机高斯分布初始化,当然实际代码实现时,比如微软的deepspeed chat在用到LoRA时,一开始通过0矩阵占位,然后调用搭配ReLU激活函数的kaiming匀称分布初始化,虽与LoRA原始定义所用的正态分布初始化差别,但此两种初始化方式都可以工作,更多先容见下面deepspeed chat的代码 ),维度上是降维
https://latex.csdn.net/eq?%5Crightarrow B是逆映射矩阵(用0矩阵初始化),维度上是升维
其中,矩阵的大小由LoRA的秩(rank)和alpha值确定
[*]参数变换:将目标层的原始参数矩阵W通过映射矩阵A和逆映射矩阵B举行变换。计算公式为:https://latex.csdn.net/eq?W%27%20%3D%20W%20+%20A%20*%20B,这里W'是变换后的参数矩阵
[*]微调模子:使用新的参数矩阵https://latex.csdn.net/eq?W%27替换目标层的原始参数矩阵https://latex.csdn.net/eq?W,然后在特定任务的训练数据上对模子举行微调
[*]梯度更新:在微调过程中,计算损失函数关于映射矩阵A和逆映射矩阵B的梯度,并使用优化算法(如Adam、SGD等)对A和B举行更新
注意,在更新过程中,原始参数矩阵W保持不变,说白了,训练的时候固定原始PLM的参数,只训练降维矩阵A与升维矩阵B
[*]重复更新:在训练的每个批次中,重复步骤3-5,直到达到预定的训练轮次(epoch)或满足收敛条件
总之,LoRA的详细步骤包括选择目标层、初始化映射矩阵和逆映射矩阵、举行参数变换和模子微调。在微调过程中,模子会通过更新映射矩阵U和逆映射矩阵V来学习特定任务的知识,从而提高模子在该任务上的性能
继承说一下,这个LoRA的应用还是挺广的,比如后续微软推出的DeepSpeed-Chat便用了这个方法
DeepSpeed-Chat的实现中,当设置LoRA的低秩维度lora_dim(如lora_dim=128)时,即认为启用了LoRA训练,则将原始模子中名称含有“deoder.layers.”且为线性层修改为LoRA层,具体操纵为:
[*]将原始结构的weight参数冻结;
[*]新引入了2个线性层lora_right_weight和lora_left_weight (分别对应上图中的降维矩阵A、升维矩阵B ),可实现先降维至lora_dim再升维回原维度;
[*]LoRA层主要实现了两分支通路,一条分支为已被冻结weight参数的原始结构、另一条分支为新引入的降维再升维线性层组
trainer.save_model(output_dir=training_args.output_dir)再额外分析下 这段代码的最后部分
if __name__ == "__main__":
train()通例部分的正向传播由transformers所定义,而LoRA部分的正向传播则由LinearLayer_LoRA(nn.Module)的forward()所定义,即“LoRA层的两条分支效果举行加和”,如下图所示『图源:https://huggingface.co/docs/peft/conceptual_guides/lora,相当于在训练期间,较小的权重矩阵(下图中的A和B)是分开的,但一旦训练完成,权重可以归并到一个新权重矩阵中 』
https://i-blog.csdnimg.cn/blog_migrate/c7f0a9e4fb3844ede3c037e1260de70f.png
在代码中体现为
def __init__(
self,
model: PreTrainedModel,
args: TrainingArguments,
train_dataset: Optional = None,
eval_dataset: Optional = None,
tokenizer: Optional = None,
data_collator: Optional = None,
train_iterator: Optional = None,
eval_iterator: Optional = None,
...
):加号左侧为原结构支路,加号右侧为新增支路,self.lora_right_weight 和self.lora_left_weight 分别为两个新引入线性层的参数
而Huggingface公司推出的PEFT(Parameter-Efficient Fine-Tuning)库也封装了LoRA这个方法,PEFT库可以使预训练语言模子高效顺应各种下游任务,而无需微调模子的所有参数,即仅微调少量(额外)模子参数,从而大大低落了计算和存储本钱
ModelFull FinetuningPEFT-LoRA PyTorchPEFT-LoRA DeepSpeed with CPU Offloadingbigscience/T0_3B (3B params)47.14GB GPU / 2.96GB CPU14.4GB GPU / 2.96GB CPU9.8GB GPU / 17.8GB CPUbigscience/mt0-xxl (12B params)OOM GPU56GB GPU / 3GB CPU22GB GPU / 52GB CPUbigscience/bloomz-7b1 (7B params)OOM GPU32GB GPU / 3.8GB CPU18.1GB GPU / 35GB CPU 且PEFT库 (peft/src/peft/peft_model.py at main · huggingface/peft · GitHub)支持以下游行的方法
[*]LoRA,PEFT对LoRA的实现封装见:peft/src/peft/tuners/lora.py at main · huggingface/peft · GitHub,比如对权重的归并代码 (和上面DSC对LoRA权重归并的实现,在本质上是一致的) for step, inputs in enumerate(epoch_iterator):
[*]Prefix Tuning: Prefix-Tuning: Optimizing Continuous Prompts for Generation, P-Tuning v2: Prompt Tuning Can Be Comparable to Fine-tuning Universally Across Scales and Tasks
[*]P-Tuning: GPT Understands, Too
[*]Prompt Tuning: The Power of Scale for Parameter-Efficient Prompt Tuning
故而,Alpaca-LoRA即可以通过PEFT库实现的LoRA方法在消耗级GPU微调「基于LLaMA的Alpaca」,比如项目中的这个文件finetune.py 包含了PEFT在LLaMA上的直接应用,以及一些与prompt construction和tokenization相关的代码,以下是用法示例:
outputs = model(**inputs) 我们还可以调整我们的超参数(为方便大家理解,我给每个参数都加了解释阐明):
loss = outputs.loss 2.3 Alpaca所用的self-instruct的影响力:解决一大批模子的数据扩展题目
很快,通过下文你会发现
[*]self-instruct启发出许多「羊驼类模子」
羊驼率先动员的self-instruct,启发后续许多人/团队也用这个方式去采集『提示ChatGPT API』的数据,比如BELLE、ChatLLaMA、ColossalChat
[*]许多「羊驼类模子」的数据被用于微调新一批模子
然后还有一批模子各种叠加组合比如『Alpaca/BELLE』,又用于微调一批批模子
比如ChatDoctor 有效到Alpaca的数据举行微调,再比如有人拿BELLE数据tuning去调chatglm
https://i-blog.csdnimg.cn/blog_migrate/5e3fa7eedeed260e86b19254ca4468a3.png
一下子出来这么新的模子 似乎有点懵,没事,请看下文及下一篇文章娓娓道来..
2.3.1 UC Berkeley的Vicuna/FastChat:通过ShareGPT.com的7万条对话数据微调LLaMA
23年3.31日,受 Meta LLaMA 和 Stanford Alpaca 项目的启发,加州大学伯克利分校(UC Berkeley)等大学的研究者根据从 ShareGPT.com (ShareGPT是一个用户可以分享他们的 ChatGPT 对话的网站)收集的用户共享对话微调 LLaMA 推出了Vicuna-13B(中文称小羊驼,GitHub所在:FastChat)
https://i-blog.csdnimg.cn/blog_migrate/42f23aaff4593859726fdf42b78cdb3f.png
在数据规模上,Vicuna从ShareGPT.com 的公共 API 收集了大约 70K 用户共享对话,且为了确保数据质量,原作者们将 HTML 转换回 markdown 并过滤掉一些不符合或低质量的样本。此外,将冗长的对话分成更小的部分,以顺应模子的最大上下文长度,并做了以下改进
[*]内存优化:为了使 Vicuna 能够理解长上下文,将最大上下文长度从羊驼Alpaca中的 512 扩展到 2048,这大大增长了 GPU 内存需求,对此通过使用梯度查抄点和闪存注意力来解决内存压力 (We tackle the memory pressure by utilizing gradient checkpointing and flash attention)
[*]多轮对话:调整训练损失以思量多轮对话,并仅根据聊天呆板人的输出计算微调损失
[*]通过Spot Instance 低落本钱:40 倍大的数据集和 4 倍的训练序列长度对训练费用提出了相当大的寻衅。原作者们使用SkyPilot managed spot 来低落本钱『SkyPilot是加州大学伯克利分校构建的一个框架,用于在各种云上轻松且经济高效地运行 ML 工作负载』,方法是使用更便宜的spot instances以及auto-recovery for preemptions and auto zone switch
该解决方案将 7B 模子的训练本钱从 500 美元削减至 140 美元左右,将 13B 模子的训练本钱从 1000 美元左右削减至 300 美元
有两点值得一提的是
[*]Vicuna的预训练是一天之内通过8个具有 80GB 显存的 A100 GPU 举行训练的,预训练好之后单纯摆设的话,Vicuna-13B 需要大约 28GB 的GPU 显存,Vicuna-7B 大约需要14GB GPU显存
[*]且Vicuna使用了和Alpaca差不多的超参数 Hyperparameter 全局批量大小
Batch Size
学习率
Learning rate
EpochsMax lengthWeight decayVicuna-13B1282e-5320480
最终通过直接使用GPT4评估之后(基于 GPT-4 的评估框架来自动评估聊天呆板人的性能),效果还不错
Model NameLLaMA(骆驼)Alpaca(羊驼)Vicuna(小羊驼)Bard/ChatGPTDatasetPublicly available datasets
(1.4T token)Self-instruct from davinci-003 API
(52K samples)User-shared conversations
(70K samples)N/ATraining codeN/AAvailableAvailableN/AEvaluation metricsAcademic benchmarkAuthor evaluationGPT-4 assessmentMixedTraining cost
(7B)82K GPU-hours$500 (data) + $100 (training)$140 (training)N/ATraining cost
(13B)135K GPU-hoursN/A$300 (training)N/A 2.3.2 链家BELLE:结合中文语料通过Self Instruct方式微调BLOOMZ-7B或LLaMA
Stanford Alpaca的种子任务都是英语,收集的数据也都是英文,因此训练出来的模子未对中文优化。为了提拔对话模子在中文上的效果,70 亿参数的中文对话大模子 BELLE『Bloom-Enhanced Large Language model Engine』来了(这是项目所在)。
在数据方面,结合以下两方面的数据:
[*]Alpaca 的 5.2 万条英文数据
[*]通过Alpaca的数据收集代码天生的约 100 万条中文数据『也仅使用由 GPT3.5 即模子text-davinci-003 生产的数据,不包含任何其他数据,如果想使用ChatGPT的API比如gpt-3.5-turbo模子,可通过参数控制』
模子训练上,有
[*]基于BLOOMZ-7B1-mt优化后的模子:BELLE-7B-0.2M,BELLE-7B-0.6M,BELLE-7B-1M,BELLE-7B-2M
[*]基于huggingface的LLaMA实例实现调优的模子:BELLE-LLAMA-7B-2M,BELLE-LLAMA-13B-2M
BLOOM是由HuggingFace于2022年3月中旬推出的大模子,规模最大版本的参数量达到176B(GPT-3是175B),基于从 Megatron-LM GPT-2修改而来的仅解码器 transformer 模子架构
https://latex.csdn.net/eq?%5Crightarrow 对应的论文为《BLOOM: A 176B-Parameter Open-Access Multilingual Language Model》(翻译之一,解读之一)
https://latex.csdn.net/eq?%5Crightarrow 此外,这里有篇不错的文章(重点讲了下Megatron-DeepSpeed):千亿参数开源大模子 BLOOM 背后的技能
至于HuggingFace是著名开源工具Transformers的开发公司,许多推理工具都会支持Transformers中的模子
截至23年3月中旬,超过100B参数量且能够支持中文的开源大模子只有BLOOM和GLM-130B
该项目主要包含以下三部分内容:
[*]175 个中文种子任务,斯坦福Alpaca一样,每个任务都包含对应的指令/任务、prompt、输出
https://latex.csdn.net/eq?%5Crightarrow zh_seed_tasks.jsonl:样例如下
{ "id": "seed_task_20", "name": "horror_movie_opening",
"instruction": "你需要为一部可怕影戏写一个创意的开场场景。",
"instances": [{"input": "","output":" 太阳已经落山,留下了一个暗中的小镇。微风吹拂空荡的街道,让每一个冒险走出门外的人感到一阵寒意。唯一的声音是被风吹动的树叶发出的轻微沙沙声。突然,一声令人不寒而栗的尖啼声划破了沉寂,随后是玻璃破裂的声音。一所房子亮起了灯光,可以看到一个人影朝镇中央奔跑。当> 那个人影越来越靠近时,清晰地看到那是一个年轻女子,她满身血迹斑斑。"}],
"is_classification": false }
[*]https://latex.csdn.net/eq?%5Crightarrow prompt_cn.txt: 天生所使用的提示语
https://latex.csdn.net/eq?%5Crightarrow 0.5M 天生的数据
[*]天生数据及其代码
沿用 Alpaca 的方式:
pip install -r requirements.txt
export OPENAI_API_KEY=YOUR_API_KEY
python generate_instruction.py generate_instruction_following_data
默认使用 Completion API,模子 text-davinci-003。如果想使用 Chat API 并使用 gpt-3.5-turbo 模子,可通过参数控制:
python generate_instruction.py generate_instruction_following_data \
--api=chat --model_name=gpt-3.5-turbo
输出文件在 Belle.train.json,可以人工筛选后再使用
[*]基于 BLOOMZ-7B1-mt 模子和 Belle.train.json 训练模子
2.4 Chinese-LLaMA/Chinese-Alpaca:通过中文数据预训练/指令微调
Chinese LLaMA(也称中文LLaMA,有7B和13B两个版本,项目所在),相当于在原版LLaMA的底子上扩充了中文词表并使用了中文数据举行二次预训练,进一步提拔了中文底子语义理解能力,同时,在中文LLaMA的底子上,且用中文指令数据举行指令精调得Chinese-Alpaca(也称中文Alpaca,同样也有7B和13B两个版本)
具体而言,主要做了以下三方面的工作
2.4.1 词表扩充中文数据
在通用中文语料上训练了基于SentencePiece的20K中文词表并与原版LLaMA模子的32K词表举行归并
排除重复的token后,得到的最终中文LLaMA词表大小为49953
需要注意的是,在fine-tune阶段Alpaca比LLaMA多一个pad token,所以中文Alpaca的词表大小为49954
这么做的主要缘故原由是原版LLaMA模子的词表大小是32K,其主要针对英语举行训练,对多语种支持不是特别理想(可以对比一下多语言经典模子XLM-R的词表大小为250K)。通过初步统计发现,LLaMA词表中仅包含很少的中文字符,所以在切词时会把中文切地更碎,需要多个byte token才能拼成一个完整的汉字,进而导致信息密度低落
其对应的扩充词表的脚本代码为 (代码所在在:merge_tokenizers.py,为方便大家更好的理解,我给每一行的代码 都加上了解释)
loss.backward() 这段代码的主要目的是将一个中文的sentencepiece模子与一个已经预训练好的LLaMA tokenizer举行归并,以便在处理中文文本时,LLaMA tokenizer能更好地举行分词
整个过程包括了加载模子、归并模子、生存新的tokenizer以及举行测试等步骤,具体如下
[*]首先,通过argparse模块获取下令行参数,包括原始的LLaMA tokenizer的路径和中文sentencepiece模子的路径
[*]接着,加载这两个模子,并将它们转换为Protocol Buffers格式,方便举行操纵
[*]然后,从中文sentencepiece模子中提取词汇,并将这些词汇添加到LLaMA tokenizer中
在这个过程中,需要查抄每个中文词汇是否已经存在于LLaMA tokenizer中,以避免重复添加
[*]将归并后的模子生存到指定的目录
即首老师存为sentencepiece模子文件,然后创建一个新的LlamaTokenizer实例,并将其生存为Hugging Face格式的tokenizer
[*]最后,对原始的LLaMA tokenizer和归并后的Chinese-LLaMA tokenizer举行测试,以验证归并是否成功
测试包括输出特别词汇、特别词汇ID、特别词汇映射等信息,以及使用这两个tokenizer对给定文本举行分词
从测试效果可以看出,归并后的Chinese-LLaMA tokenizer能够更好地处理中文文本
此外,七月在线ChatGPT原理剖析课一学员在群内问道:“怎样扩充词表、训练embedding,然后再与llama的归并,想在自己的数据上试试”
“吹牛班的春天”答道:“我知道的方法就是直接改embedding结构:初始化参数concat到以前embedding层上,以前的权embedding权重就保留,多出来的部分就背面更新,下图是以前BERT无损扩词的思绪,可做参考”
https://i-blog.csdnimg.cn/blog_migrate/eca4ded60aceb344cff3fba22772cc89.png
思量到再厥后24年6月份,我司七月的「大模子线上营」中又有学员问到了这个题目,阿荀则详细的答复了下
https://i-blog.csdnimg.cn/blog_migrate/98a608945068ce8c750c589e8c4fedbf.png
至于如果自己扩建词汇表后,新的词汇表内里还是会存在没故意义的字符文字,有什么办法可以优化吗?这个还是要先看缘故原由的,大概有几个方面
[*]大概是根本的语料题目,新词是根据语料统计得到的,反过来说是语料中的字符频率信息决定新词的发掘,大概要看语料自己有没有题目
[*]大概是具体实现题目,实施的时候vocabsize一类的阈值参数设置太大,导致连带一些确实没什么意义的长尾词都纳入进新词中了
[*]也大概是对“无意义”的定义题目,如果你用的是bbpe,也比力容易得到一些看上去不太合乎语言学层面的新词,但实际并没有什么影响
2.4.2 加入中文数据的预训练
在预训练阶段,使用约20G左右的通用中文语料(与中文BERT-wwm、MacBERT、LERT、PERT中使用的语料一致)在原版LLaMA权重的底子上进一步举行预训练。该过程又分为两个阶段:
第一阶段:冻结transformer参数,仅训练embedding,在只管不干扰原模子的情况下适配新增的中文词向量
第二阶段:使用LoRA技能,为模子添加LoRA权重(adapter),训练embedding的同时也更新LoRA参数
2.4.3 指令精调
指令精调阶段的任务情势基本与Stanford Alpaca相同,训练方案同样采用了LoRA举行高效精调,并进一步增长了可训练参数数量
在prompt设计上,精调以及预测时采用的都是原版Stanford Alpaca不带input的模版。对于包含input字段的数据,采用" f{instruction}+\n+{input} "的情势举行拼接
且指令精调阶段使用了以下数据,其中7B模子约2M数据、13B模子约3M数据。基本构成如下:
数据量级来源阐明中英翻译数据500K外部链接在原数据集的底子上举行了采样+规则筛选pCLUE数据300K外部链接在原数据集的底子上举行了采样+规则筛选Alpaca数据(英)50K外部链接斯坦福原版Alpaca训练数据Alpaca数据(中)50K本地链接本项目使用ChatGPT接口将英文版翻译为中文(筛掉一部分)Self-instruction数据1~2M(暂无)本项目使用ChatGPT接口举行爬取,提供了一个动态天生差别范畴和指令类型的prompt爬取脚本script/crawl_prompt.py。 python script/crawl_prompt.py output-file
思绪与Stanford Alpaca中的做法基本一致,一次批量天生20组数据(可自行修改模板),以低落爬取本钱
天生的文件包含通过gpt-3.5-turbo爬取的数据(你必须拥有OpenAI API key才可以使用)
虽然指令模板中要求输出JSON,但系统并不总是会返回正当的JSON,需要自行对返回数据举行洗濯
由于爬取时间比力长,发起背景运行该脚本,且多线程运行时注意OpenAI API的调用限定上限
当然,针对一些任务上效果欠好!原作者也给出了几个大概的缘故原由,
1)自己LLaMA对中文支持不是很好,大多数相关衍生工作是直接在原版上举行pretrain/finetune的,而我们接纳了更大胆的计谋——增长中文词表,大概进一步加剧中文训练不充实的题目,但从久远看是否有利于后续进一步预训练就得靠时间查验了;
2)指令数据的质量有待进一步提拔;
3)训练时间、超参等方面还有很大调整空间;
4)没有RLHF;
5)4-bit量化后效果大概会降落,因此可以实验加载FP16模子,效果相对更好一些(也更慢)
2.5 姜子牙系列模子Ziya-LLaMA-13B-v1
2.5.1 基本信息:软件依赖/模子分类
姜子牙通用大模子V1是基于LLaMa的130亿参数的大规模预训练模子 (论文所在:Zero-Shot Learners for Natural Language Understanding via a Unified Multiple Choice Perspective),具备翻译,编程,文天职类,信息抽取,摘要,文案天生,常识问答和数学计算等能力。目前姜子牙通用大模子已完成大规模预训练、多任务有监督微调和人类反馈学习三阶段的训练过程「large-scale continual pre-training (PT), multi-task supervised fine-tuning (SFT), and human feedback learning (RM, PPO)」
软件依赖
if (step + 1) % self.args.gradient_accumulation_steps == 0:模子分类 Model Taxonomy
需求 Demand任务 Task系列 Series模子 Model参数 Parameter额外 Extra通用 GeneralAGI模子姜子牙 ZiyaLLaMA13BEnglish&Chinese 2.5.2 模子的预训练与微调:预训练、SFT、HFFT
继承预训练 Continual pretraining
[*]数据方面
原始数据包含英文和中文,其中英文数据来自openwebtext、Books、Wikipedia和Code,中文数据来自洗濯后的悟道数据集、自建的中文数据集。在对原始数据举行去重、模子打分、数据分桶、规则过滤、敏感主题过滤和数据评估后,最终得到125B tokens的有效数据「After deduplication, model scoring, data bucketing, rule filtering, sensitive topic filtering, and data evaluation, we finally obtained 125 billion tokens of valid data」
[*]分词方面
为了解决LLaMA原生分词对中文编解码服从低下的题目,我们在LLaMA词表的底子上增长了7k+个常见中文字,通过和LLaMA原生的词表去重,最终得到一个39410大小的词表,并通过复用Transformers里LlamaTokenizer来实现了这一效果「We achieved this by reusing the LlamaTokenizer in Transformers」
[*]训练过程
在增量训练过程中,我们使用了160张40GB的A100,采用2.6M tokens的训练集样本数量和FP 16的混淆精度,吞吐量达到118 TFLOP per GPU per second。因此我们能够在8天的时间里在原生的LLaMA-13B模子底子上,增量训练110B tokens的数据
训练期间,虽然遇到了呆板宕机、底层框架bug、loss spike等各种题目,但我们通过快速调整,保证了增量训练的稳固性。我们也放出训练过程的loss曲线,让大家了解大概出现的题目 https://i-blog.csdnimg.cn/blog_migrate/12467f948d12df8da99486b27d33999b.png
多任务有监督微调 Supervised finetuning
在多任务有监督微调阶段,采用了课程学习(curiculum learning)和增量训练( incremental training)的计谋,用大模子辅助划分已有的数据难度,然后通过“Easy To Hard”的方式,分多个阶段举行SFT训练
SFT训练数据包含多个高质量的数据集,均经过人工筛选和校验:
[*]Self-Instruct构造的数据(约2M):BELLE、Alpaca、Alpaca-GPT4等多个数据集
[*]内部收集Code数据(300K):包含leetcode、多种Code任务情势
[*]内部收集推理/逻辑相关数据(500K):推理、申论、数学应用题、数值计算等
[*]中英平行语料(2M):中英互译语料、COT类型翻译语料、古文翻译语料等
[*]多轮对话语料(500K):Self-Instruct天生、任务型多轮对话、Role-Playing型多轮对话等
人类反馈学习 Human-Feedback training
为了进一步提拔模子的综合表现,使其能够充实理解人类意图、减少“幻觉”和不安全的输出,基于指令微调后的模子,举行了人类反馈训练(Human-Feedback Training,HFT)。在训练中,我们采用了以人类反馈强化学习(RM、PPO)为主,结合多种其他手段团结训练的方法用来弥补PPO方法的短板、加速训练,具体包括
[*]人类反馈微调(Human-Feedback Fine-tuning,HFFT)
[*]后见链微调(Chain-of-Hindsight Fine-tuning,COHFT)
[*]AI反馈(AI Feedback)
[*]基于规则的奖励系统(Rule-based Reward System,RBRS)等
我们在内部自研的框架上实现了HFT的训练流程,该框架可以使用最少8张40G的A100显卡完成Ziya-LLaMA-13B-v1的全参数训练。在PPO训练中,我们没有限定天生样本的长度,以确保长文本任务的奖励准确性。每次训练的总履历池尺寸超过100k样本,确保了训练的充实性
2.5.3 效果评估、使用阐明、微调示例
https://i-blog.csdnimg.cn/blog_migrate/61e66dab965c28847a5279e1c964d560.png
使用 Usage
思量到LLaMA权重的许可限定,我们无法直接发布完整的模子权重。因此,我们使用了开源的FastChat工具,并进一步优化它以计算Ziya-LLaMA-13B-v1权重和原始LLaMA权重之间的差异( we utilized the open-source FastChat tool and further optimized it to calculate the differences between Ziya-LLaMA-13B-v1 weights and the original LLaMA weights)。用户可以按照以下步骤获得Ziya-LLaMA-13B-v1的完整权重:
[*]Step 1:获取LLaMA权重并转成Hugging Face Transformers模子格式,可参考转换脚本(若已经有huggingface权重则跳过) self.optimizer.step()
[*]Step 2:下载Ziya-LLaMA-13B-v1的delta权重以及step 1中转换好的原始LLaMA权重,使用如下脚本转换:https://github.com/IDEA-CCNL/Fengshenbang-LM/blob/main/fengshen/utils/apply_delta.py self.lr_scheduler.step()
[*]Step 3: 加载step 2得到的模子推理 def evaluate(
self, eval_dataset: Optional = None, ignore_keys: Optional] = None
) -> Dict:
微调示例 Finetune Example
Refer to ziya_finetune
推理量化示例 Inference & Quantization Example
Refer to ziya_inference
2.6 基于LLaMA微调的各模子对比:Alpaca/Vicuna/BELLE/Chinese-LLaMA/Ziya-LLaMA-13B
项目一句话描述 Stanford Alpaca
结合英文语料通过Self Instruct方式微调LLaMA 7B
Vicuna-13B
通过ShareGPT.com的7万条对话数据微调LLaMABELLE 结合中文语料通过Self Instruct方式微调BLOOMZ-7B或LLaMA
Chinese-LLaMA/Chinese-Alpaca
通过中文数据预训练/指令微调LLaMA姜子牙系列模子Ziya-LLaMA-13B-v1基于LLaMA-13B的中英文模子ChatLLaMA(英文版) LLaMA的RLHF版
ColossalChat
通过self-instruct技能指令微调LLaMA且加上RLHF
第三部分 更强的LLaMA 2开源,可直接商用
3.1 LLaMA 2简介:相比LLaMA1代——1.4倍token,2倍上下文
3.1.1 LLaMA 2:32K词表、2T训练数据、4K上下文长度、模子种类
23年7月份,Meta发布LLAMA 2 (项目所在、论文所在、论文解读之一),是 LLAMA 1 的更新版本
[*]模子结构
采用了 Llama 1 的大部分预训练设置和模子架构,比如使用标准Transformer 架构,使用 RMSNorm 应用预归一化、使用 SwiGLU 激活函数和旋转位置嵌入RoPE
[*]32K词表大小 继承沿用Llama1 所用的通过SentencePiece实现的BPE,且整个词表的大小依然为 32K
We use the same tokenizer as Llama 1; it employs a bytepair encoding (BPE) algorithm(Sennrich et al., 2016) using the implementation from SentencePiece (Kudo and Richardson, 2018). As with Llama 1, we split all numbers into individual digits and use bytes to decompose unknown UTF-8 characters. The total vocabulary size is 32k tokens.
[*]2T训练数据
使用一种新的混淆的公开可用数据举行训练,训练数据规模是2T个token(即2万亿个token),相比1代的1.4T多出了40%
[*]4K上下文长度
上下文长度达到了4096,相比1代的2048直接翻了一倍
[*]模子种类:7B、13B、70B(用了GQA)
目前 LLAMA 2 的系列模子有 7B、13B、70B 三种(34B的后续发布)
值得特别注意的是,其中的70B模子采用了分组查询注意力(grouped-query attention,简称GQA,关于什么是GQA,详见此文 ),至于其中的7B和13B则没用GQA「Transformer原始论文中用的多头注意力(MHA)、ChatGLM2-6B则用的多查询注意力(Multi-query attention,简称MQA)」 https://i-blog.csdnimg.cn/blog_migrate/6f44098534b4208ee493fc4d2e1d5886.png
3.1.2 LLaMA 2-Chat:三个版本——7B 13B 70B
同时 Meta 还发布了 LLaMA 2-CHAT,其是基于 LLAMA 2 针对对话场景微调的版本,同样 7B、13B 和 70B 参数三个版本,具体的训练方法与ChatGPT类似
https://i-blog.csdnimg.cn/blog_migrate/9d0bd36b1dee7dda8653e3c1a43619d6.png
[*]先是监督微调LLaMA2得到SFT版本 (接受了成千上万个人类标注数据的训练,本质是题目-答案对 )
[*]然后使用人类反馈强化学习(RLHF)举行迭代优化
先训练一个奖励模子
然后在奖励模子/优势函数的指引下,通过拒绝抽样(rejection sampling)和近端计谋优化(PPO)的方法迭代模子的天生计谋
LLAMA 2 的性能表现更加接近 GPT-3.5,Meta 也承认距离 GPT-4 和 PaLM 2 等领先非开源模子还有差距
https://i-blog.csdnimg.cn/blog_migrate/a535149e2b94c01e0253f9c85abab003.png
Meta 在技能报告中详细列出了 LLAMA 2 的性能、测评数据,以及分享了重要的训练方法,具体详见原论文
https://i-blog.csdnimg.cn/blog_migrate/535ae1e4ff94e7a885ef065044b22810.png
3.1.3 LLaMA 2 70B之分组查询注意力——Grouped-Query Attention
自回归解码的标准做法是缓存序列中先前标志的键 (K) 和值 (V) 对,从而加速注意力计算速率
然而,随着上下文窗口或批量大小的增长,多头注意力 (MHA)模子中与 KV 缓存大小相关的内存本钱显着增长
对于较大的模子,KV 缓存大小成为瓶颈,键和值投影可以在多个头之间共享,而不会大幅低落性能,可以使用
[*]具有单个 KV 投影的原始多查询格式(MQA)
ChatGLM2-6B即用的这个,详见此文《ChatGLM两代的摆设/微调/实现:从基座GLM、ChatGLM的LoRA/P-Tuning微调、6B源码解读到ChatGLM2的微调与实现》的3.1.2节
不外,多查询注意(Multi-query attention,简称MQA)只使用一个键值头,虽大大加速了解码器推断的速率,但MQA大概导致质量降落,而且仅仅为了更快的推理而训练一个单独的模子大概是不可取的
[*]或具有多个 KV 投影的分组查询注意力(grouped-query attention,简称GQA),速率快 质量高
23年,还是Google的研究者们提出了一种新的方法,即分组查询注意(GQA,论文所在为:GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints),这是一种多查询注意的泛化,它通过折中(多于一个且少于查询头的数量,比如4个)键值头的数量,使得经过强化训练的GQA以与MQA相当的速率达到接近多头注意力的质量 https://i-blog.csdnimg.cn/blog_migrate/a39894a0e96e93ef0e31e1a670299883.png
经实验论证,GQA 变体在大多数评估任务上的表现与 MHA 基线相当,并且平均优于 MQA 变体
更多请拜见《一文通透各种注意力:从多头注意力MHA到分组查询注意力GQA、多查询注意力MQA》
3.2 Llama 2-Chat中的RLHF:依然是三阶段训练方式
3.2.1 监督微调(SFT)
在SFT的数据上
[*]他们先是重点收集了几千个高质量 SFT 数据示例 (注意:许多新闻稿会说SFT的数据达到百万以上,这就是没仔细看论文的效果,论文之意是赛过百万低质量的数据,As a result, we focused first on collecting several thousand examples of high-quality SFT data. By setting aside millions of examples from third-party datasets and using fewer buthigher-quality examples from our own vendor-based annotation efforts, our results notably improved)
[*]之后发现几万次的SFT标注就足以获得高质量的效果,最终统共收集了27540条用于SFT的标注数据 (We found that SFT annotations in the order of tens ofthousands was enough to achieve a high-quality result. We stopped annotating SFT after collecting a total of 27,540 annotations.)
在微调过程中
[*]每个样本都包括一个prompt和一个response(说白了,就是题目-答案对,和instructGPT/ChatGPT自己的监督微调是一个本质),且为确保模子序列长度得到正确填充,Meta 将训练会合的所有prompt和response毗连起来。他们使用一个特别的 token 来分隔prompt和response片段,使用自回归目标,未来自用户提示的 token 损失归零,因此只对答案 token 举行反向传播,最后对模子举行了 2 次微调
[*]微调过程中的参数则云云设置:we use a cosine learning rate schedule with an initiallearning rate of 2 ×10−5 , a weight decay of 0.1, a batch size of 64, and a sequence length of 4096 token
https://i-blog.csdnimg.cn/blog_migrate/548948718d4c0bc9ed71e58694b12d1b.png
3.2.2 训练两个奖励模子:一个偏实用 一个偏安全
下表 6 报告了 Meta 长期以来收集到的奖励建模数据的统计效果,并将其与多个开源偏好数据集举行了对比。他们收集了超过 100 万个基于人类应用指定准则的二元比力的大型数据集,也就是奖励建模数据
https://i-blog.csdnimg.cn/blog_migrate/d79f33c260c877a4ab888549047c15f7.png
关于奖励数据
[*]prompt和response中的标志数因文本范畴而异,比如摘要和在线论坛数据的prompt通常较长,而对话式的prompt通常较短。与现有的开源数据集相比,本文的偏好数据具有更多的对话回合,平均长度也更长
[*]奖励模子将模子相应及其相应的提示(包括前一轮的上下文)作为输入,并输出一个标量分数来表现模子天生的质量(例如有效性和安全性),使用这种作为奖励的相应得分,Meta 在 RLHF 期间优化了 Llama 2-Chat,以更好地与人类偏好保持一致,并提高有效性和安全性
在每一批用于奖励建模的人类偏好标注中,Meta 都拿出 1000 个样本作为测试集来评估模子,并将相应测试集的所有prompt的集合分别称为实用性和安全性 (许多新闻稿会翻译成元实用、元安全,其实没须要加个“元”字,你理解为是Meta内部定义的“实用”与“安全”两个概念即可)
故为了分身和平衡模子的实用性和安全性,LLaMA 2团队训练了两个独立的奖励模子
[*]一个针对实用性(称为实用性RM)举行了优化,在内部所有偏实用的奖励数据集上举行训练,并结合从内部偏安全的奖励数据集和开源安全性数据会合统一采样的划一部分剩余数据
theHelpfulness reward model is eventually trained on all Meta Helpfulness data, combined with an equalparts of the remaining data uniformly sampled from Meta Safety and from the open-source datasets
[*]另一个针对安全性(安全性RM)举行了优化,在内部所有偏安全的奖励数据和人类无害数据上举行训练,并以90/10的比例混淆内部偏实用的奖励数据和开源实用性数据
TheMeta Safety reward model is trained on all Meta Safety and Anthropic Harmless data, mixed with MetaHelpfulness and open-source helpfulness data in a 90/10 proportion.
We found that the setting with 10% helpfulness data is especially beneficial for the accuracy on samples where both the chosen and rejectedresponses were deemed safe
并通过预训练的LLaMA 2初始化奖励模子(意味着奖励模子的架构与参数与预训练模子一致,只是用于下一个token预测的分类头被替换为用于输出标量奖励的回归头),由于它确保了两个模子都能从预训练中获得的知识中受益
「虽然论文中的原文是:We initialize our reward models from pretrained chat model checkpoints, as it ensures that both modelsbenefit from knowledge acquired in pretraining,以及The model architecture and hyper-parameters are identical to thoseof the pretrained language models, except that the classification head for next-token prediction is replacedwith a regression head for outputting a scalar reward,但为何没有类似ChatGPT那样,通过微调过的SFT初始化RM模子,该点存疑 」
为了使模子活动与人类偏好相一致,Meta 收集了代表了人类偏好履历采样的数据,通过针对同一个prompt模子给出的两个差别的response,人类标注者选择他们更喜欢的模子输出。这种人类偏好被用于训练奖励模子
https://latex.csdn.net/eq?%5Cmathcal%7BL%7D_%7B%5Ctext%20%7Branking%20%7D%7D%3D-%5Clog%20%5Cleft%28%5Csigma%5Cleft%28r_%7B%5Ctheta%7D%5Cleft%28x%2C%20y_%7Bc%7D%5Cright%29-r_%7B%5Ctheta%7D%5Cleft%28x%2C%20y_%7Br%7D%5Cright%29%5Cright%29%5Cright%29
其中,https://latex.csdn.net/eq?y_c是标注者选择的首选response,https://latex.csdn.net/eq?y_r是被拒绝的对应response
且为了让模子可以更好的了解到差别response质量之间的差异,作者团队将偏好评级被分为4层评级,且思量到根据这些评级信息使得奖励模子对有更多差异的天生,有着差别分数且这些分数上相互之间的差距尽大概拉开是有效的「Given that our preference ratings is decomposed as a scale of four points (e.g.,significantly better), it can be useful to leverage this information to explicitlyteach the reward model to assign more discrepant scores to the generations that have more differences」,为此,我们在损失中进一步添加一个边际成分
https://latex.csdn.net/eq?%5Cmathcal%7BL%7D_%7B%5Ctext%20%7Branking%20%7D%7D%3D-%5Clog%20%5Cleft%28%5Csigma%5Cleft%28r_%7B%5Ctheta%7D%5Cleft%28x%2C%20y_%7Bc%7D%5Cright%29-r_%7B%5Ctheta%7D%5Cleft%28x%2C%20y_%7Br%7D%5Cright%29-m%28r%29%5Cright%29%5Cright%29
其中边际https://latex.csdn.net/eq?m%28r%29是偏好评级的离散函数,他们发现这个边际成分可以提高资助性奖励模子的准确性,特别是在两个response更好区分的的样本上「where the margin m(r) is a discrete function of the preference rating. We found this margin component can improve Helpfulness reward model accuracy especially on sampleswhere two responses are more separable」
具体而言,为了衡量差别response优劣的程度,划分为4个品级(比如很好、好、较好、一样寻常好或不确定),那这4个品级是需要有一定的隔断的,相互之间不能模棱两可(模棱两可就容易把模子搞糊涂),而这个隔断大小是个超参数,可以人为设定,比如为小点的隔断1/3或大点的隔断1,如下图所示
https://i-blog.csdnimg.cn/blog_migrate/7ce56df48c88b67e4d8855c7c0b3c70f.png
3.2.3 具体的计谋迭代:PPO与拒绝采样
此处使用两种主要算法对 RLHF 举行了微调:
[*]近端计谋优化(PPO)
PPO在之前的《ChatGPT技能原理剖析》或《强化学习极简入门》等文章中讲的很详细了,可以拜见
[*]拒绝采样(Rejection Sampling)
即在模子天生多个回复后,选择最佳的回复作为模子的输出,过程中,如果天生的回复不符合预期,就会被拒绝,直到找到最佳回复
从而资助提高模子的天生质量,使其更符合人类的期望
At each iterative stage, we sample K answers for each prompt from the most recent model. We score eachsample given the best reward model accessible at the time of the experiment, and then select the best answerfor a given prompt
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页:
[1]