TensorRT-LLM包括以下一系列的特性,是当前大模型部署必备神器:
确保CUDA版本为12.x,否则先进行升级
当前已经不需要自己编译TensorRT-LLM(非常耗时),官方提供了编译好的Docker。
所以直接下载官方Triton23.10的镜像,选择内置tensorrt-llm和Python的版本
1 |
|
启动并进入容器
1 |
|
安装TensorRT-LLM
1 |
|
这里以Baichuan7B-V1-Base为例
1 |
|
转换好之后,可以执行run.py
进行初步验证
1 |
|
下面结合triton进行模型的部署
首先准备相关文件
1 |
|
现在triton_model_repo
目录的结构,如下所示:
修改preprocessing, tensorrt_llm, postprocess中的config.pbtxt
可以参考: https://github.com/triton-inference-server/tensorrtllm_backend#modify-the-model-configuration
一些注意点
tokenizer_type
为auto
,同时需要修改中的model.py,添加trust_remote_code=True
max_tokens_in_paged_kv_cache
会覆盖kv_cache_free_gpu_mem_fraction
; enable_trt_overlap
设置为Falsemax_batch_size
需要与preprocessing,tensorrt_llm,postprocess相同然后启动triton服务
1 |
|
直接用curl请求ensemble的HTTP接口进行测试:
1 |
|
返回json格式
1 |
|
也可以只请求tensort_llm服务,自己完成预处理和后处理
1 |
|
以上这些问题都可以基于Scaling Law的理论进行回答。本文是阅读了一系列caling Law的文章后的整理和思考,包括Scaling Law的概念和推导以及反Scaling Law的场景,不当之处,欢迎指正。
大模型的Scaling Law是OpenAI在2020年提出的概念[1],具体如下:
对于Decoder-only的模型,计算量$C$(Flops), 模型参数量$N$, 数据大小$D$(token数),三者满足: $C ≈ 6ND$ 。(推导见本文最后)
模型的最终性能主要与计算量$C$,模型参数量$N$和数据大小$D$三者相关,而与模型的具体结构(层数/深度/宽度)基本无关。
固定模型的总参数量,调整层数/深度/宽度,不同模型的性能差距很小,大部分在2%以内
为了提升模型性能,模型参数量$N$和数据大小$D$需要同步放大,但模型和数据分别放大的比例还存在争议。
Scaling Law不仅适用于语言模型,还适用于其他模态以及跨模态的任务[4]:
这里横轴单位为PF-days: 如果每秒钟可进行$10^{15}$次运算,就是1 peta flops,那么一天的运算就是$10^{15} × 24 × 3600 = 8.64 × 10^{19}$,这个算力消耗被称为1个petaflop/s-day。
$$ L(x) = L_{\infty} + (\frac{x_{0}}{x})^{\alpha}$$
根据公式,增大$x$(例如计算量$C$),模型整体loss下降,模型性能提升;伴随$x$趋向于无穷大,模型能完美拟合数据的真实分布,让第二项逼近0,整体趋向于$L_{\infty}$
下图是GPT4报告[5]中的Scaling Law曲线,计算量$C$和模型性能满足幂律关系
横轴是归一化之后的计算量,假设GPT4的计算量为1。基于10,000倍小的计算规模,就能预测最终GPT4的性能。
纵轴是”Bits for words”, 这也是交叉熵的一个单位。在计算交叉熵时,如果使用以 2 为底的对数,交叉熵的单位就是 “bits per word”,与信息论中的比特(bit)概念相符。所以这个值越低,说明模型的性能越好。
下图是Baichuan2[6]技术报告中的Scaling Law曲线。基于10M到3B的模型在1T数据上训练的性能,可预测出最后7B模型和13B模型在2.6T数据上的性能
下图是MindLLM[7]技术报告中的Scaling Law曲线。基于10M到500M的模型在10B数据上训练的性能,预测出最后3B模型在500B数据上的性能。
根据幂律定律,模型的参数固定,无限堆数据并不能无限提升模型的性能,模型最终性能会慢慢趋向一个固定的值
如图所示,如果模型的参数量为$10^3$(图中紫色的线),在数量达到$10^9$,模型基本收敛。所以在数据量达到$10^9$后,继续增加数据产生的计算量,没有同样计算量下提升模型参数量带来的收益大。(计算效率更优)。根据$C=6ND$,可以进一步转换成模型参数与计算量的关系,即: 模型参数为$10^3$,在计算量为$6 \times 10^{12}$ Flops,即$7 \times 10^{-8}$ PF-days时基本收敛。也就是右图中紫色线的拐点。
按照上面的思路,下面进行Scaling Law的实操
首先准备充足的数据(例如1T),设计不同模型参数量的小模型(例如0.001B - 1B),独立训练每个模型,每个模型都训练到基本收敛(假设数据量充足)。根据训练中不同模型的参数和数据量的组合,收集计算量与模型性能的关系。然后可以进一步获得计算效率最优时,即同样计算量下性能最好的模型规模和数据大小的组合,模型大小与计算量的关系,以及数据大小与计算量的关系。
如图所示,根据左图可以看到计算量与模型性能呈现幂律关系(可以认为数据和模型都不受限制),根据中图和右图,可以发现$N_{opt} \propto C^{a}, D_{opt} \propto C^{b}$,即计算效率最优时,模型的参数与计算量的幂次成线性关系,数据量的大小也与计算量的幂次成线性关系。
根据$C=6ND$,可以推算出$a+b=1$,但是$a,b$分别是多少存在分歧。
OpenAI[1]认为模型规模更重要,即$a=0.73, b=0.27$,而DeepMind在Chinchilla工作[2]和Google在PaLM工作[3]中都验证了$a=b=0.5$,即模型和数据同等重要。
所以假定计算量整体放大10倍,OpenAI认为模型参数更重要,模型应放大$10^{0.73}$ (5.32)倍,数据放大 $10^{0.27}$ (1.86)倍;后来DeepMind和Google认为模型参数量与数据同等重要,两者都应该分别放大 $10^{0.5}$ (3.16)倍。
例如在PaLM的实验中,计算量从$1 \times 10^{21}$放大10倍到$1 \times 10^{22}$, 模型参数提升了3.2倍,3.35B->10.7B。
具体最好在自己的数据上做实验来获得你场景下的$a$和$b$。
假设我们遵循计算效率最优来研发LLM,那么根据Scaling Law,给定模型大小,可以推算出最优的计算量,进一步根据最优计算量就能推算出需要的token数量,然后训练就行。
但是计算效率最优这个观点是针对训练阶段而言的,并不是推理阶段。
Meta在LLaMA[8]的观点是:给定一个模型的目标性能,并不需要用最优的计算效率在最快时间训练好模型,而应该在更大规模的数据上,训练一个相对更小模型,这样的模型在推理阶段的成本更低,尽管训练阶段的效率不是最优的(同样的算力其实能获得更优的模型,但是模型尺寸也会更大)。所以尽管根据Scaling Law,10B模型只需要200B的数据,但是作者发现7B的模型性能在1T的数据后还能继续提升。
所以LLaMA工作的重点是训练一系列语言模型,通过使用更多的数据,让模型在有限推理资源下有最佳的性能。
具体而言,确定模型尺寸后,Scaling Law给到的只是最优的数据供给,或者说是一个至少的数据量,实际上观察在各个指标上的性能表现,只要还在继续增长,就可以持续增加训练数据。
对于Decoder-only的模型,计算量$C$(Flops), 模型参数量$N$(除去Embedding部分), 数据大小$D$(token数), 三者的关系为: $C ≈ 6ND$
推导如下,记模型的结构为:
首先推导模型的参数量$N$(忽略embedding,norm和bias)计算如下:
transformer每层包括: self-attetion 和 MLP 两个部分:
所以每层的参数量为: $4d^2 + 8d^2 = 12d^2$,全部的$l$层的参数量为: $12ld^{2}$,即$N=12ld^{2}$
继续推导模型的前向推理的计算量:
计算量的单位是FLOPs,floating point operations
对于矩阵$A \in \mathbb{R}^{m \times n}, B \in \mathbb{R}^{n \times p}$,$AB$相乘的计算量为$2mnp$,一次加法一次乘法。
假设Decoder层的输入$X \in \mathbb{R}^{b \times s \times d}$, $b$为batch size,$s$为序列长度, $d$为模型维度。
所以整个decoder层的计算量为:$24bsd^2 + 4bs^2d$,全部$l$层为: $C_{forward} = 24lbsd^2 + 4lbs^2d$
反向传播计算量是正向的2倍,所以全部的计算量为: $C = 3*C_{forward} = 72lbsd^2 + 12lbs^2d$
平均每个token的计算量为 $C_{token} = \frac{C}{bs} = 72ld^2 + 12lsd = 6N(1+\frac{s}{6d}) \approx 6N$ ($s \ll 6d$)
所以对于全部包含$D$个token的数据集: $C = C_{token}D \approx 6ND$
数据集 | 语言 | 发布机构 | 发布时间 | 数据规模 | 数据来源 | 训练的模型 |
---|---|---|---|---|---|---|
Pile[1] | 英文 | Pile | 2020 | 800GB | 22个不同的数据集,包括百科/代码等 | PaLM, Chinchilla |
RefinedWeb[2] | 英文 | EleutherAI | 2023 | 600B token | 网络数据CC | Falcon |
ChineseWebText[3] | 中文 | 中科院自动化所 | 2023 | 1.42TB | 网络数据CC |
Pile是EleutherAI发布的一个英文的预训练语料。涵盖了22个不同的数据来源,包括作者新构建的ArXiv, GitHub等,还包括已有的Books3,English Wikipedia等数据集。
实验表明:
RefinedWeb是Falcon发布的一个英文的预训练语料,仅包含网络数据,共5T,开源了其中的600B数据。
作者的核心观点是,不需要精心手工构造数据源(百科,书籍等),而是直接采用大规模高质量的网络数据即可。实验表明,仅通过高质量的网络数据训练出的模型比传统手工构建的数据集效果更好。
即Pile之所以能比网络数据C4效果好,不是网络数据的问题,而是C4的数据规模太小,清洗规则有偏。同样的网络数据RefinedWeb就比Pile更好。
这个工作的核心如何对网络数据进行清洗,进而无偏地提取出高质量的网络数据。作者提出的pipeline包括:
结果显示,仅使用web数据,并且排除了高质量的传统数据源(百科,code等),效果依然比手工精心构建的不同数据源的训练数据要好。
ChineseWebText是按照RefinedWeb的思路做的中文版本,由中科院自动化所发布,仅包含网络数据,共1.42TB,并且每条数据都有一个打分,其中高质量数据有600GB。
作者提出了EvalWeb的数据处理pipeline,包括下面几个步骤:
通过对每条文本基于BERT进行打分,基于阈值为0.4过滤出高质量文本600GB。基于这个阈值过滤的文本,人工评估显示90%均为高质量的文本。
不过相比RefinedWeb,本文没有具体的训练模型进行对比,所以基于该数据集训练的模型实际效果还有待验证。
(1) reward模型很有必要
reward模型评估效果理论上能逼近于人工评估,并显著优于传统的BLUE等指标。所以从这个角度看,即使不做后面RL部分,获得一个靠谱的reward的模型也对业务有很大帮助。基于reward模型,可以评估现有模型,过滤训练数据中的脏数据等等。
(2) 突破标注者的上限
生成任务往往是没有标准答案的,所以模型的上限直接取决于标注者的标注水平。以机器翻译为例,能翻译出“信达雅”标注人员是非常昂贵的,所以给到的SFT数据一般是普通的标注人员标注的。所以如果只做SFT阶段,模型是有天花板的,通过RL可以让模型突破天花板,生成的效果有机会能超过人类标注者。
推荐trl,hugginceface出品。如果你习惯hugginceface的trainer,trl上手会非常快,并且同时支持PPO和DPO两种算法,也有丰富的例子和文档。
Reward模型本质是一个句子级的分类,对于baichuan模型,需要手工添加类BaiChuanForSequenceClassification
,可参考LlamaForSequenceClassification。
Reward模型需要基于SFT模型进行初始化。如果是基于lora进行训练,lora的task type需设置为TaskType.SEQ_CLS
.
reward部分的难点是如何收集偏好数据,具体需要:
这里可以参考OpenAI的paperLearning to summarize from human feedback,里面有如何标注偏好数据的详细描述。
另外,如果想用GPT4替代人工进行标注,需要仔细优化prompt和二次验证。数据质量 >> 数据数量。
如果SFT模型和偏好数据一切正常,就可以获得如下的预期结果: loss下降,accuracy上升
根据baichuan/llama的技术报告,一般reward模型的准确率到80%,就算是一个还不错的reward模型。 如果reward训崩了,大概率是数据的问题,需要严格检查数据质量。
有了相对不错的reward模型就可以进入到RL部分,PPO是目前最广泛应用的RL算法,也比较复杂。
理论部分,推荐TRiddle大佬写的解析文章:拆解大语言模型RLHF中的PPO
具体而言,PPO包括4个模型: actor(真正优化的,需训练), cirtic(评估的,需训练), reward(上一步获得的reward模型,无需训练), ref(SFT模型,无需评估)
PPO的训练目标如下: 第一项的reward要尽可能大,同时第二项与SFT模型的偏移要尽可能小
对照读trl实现的源码: https://github.com/huggingface/trl/blob/main/trl/trainer/ppo_trainer.py
PPO可以分成三个阶段: 采样,反馈 和 学习
1 |
|
PPO的训练是不稳定的,同样的参数和数据,不同随机数跑多次,结果差异也会很大,例如:
具体可调节的参数非常多,下面是一些可以尝试的项:
1 |
|
具体实践上,有一些小tips可供参考:
1 |
|
如果一切顺利+运气不错,可以观测到相对稳定的reward提升,同时kl也没有很大偏移
如果觉得PPO不稳定,太复杂,那可以试试DPO(Direct Preference Optimization),同样有效,但简单很多!
DPO无需reward模型,无需RL,也无需4个模型,就1个模型,直接在偏好数据上进行监督训练。
DPO的paper为: https://arxiv.org/abs/2305.18290
DPO的训练目标如下:
可以认为希望左半部分和右半部分的margin越大越好。左半部分是「优化模型」相比「原始SFT模型」在「偏好数据中good case上」的差值;右半部分是「优化模型」相比「原始SFT模型」在「偏好数据中bad case上」的差值。
所以margin变大有下面几种情况:
(1) 左边变大,右边变小: 「good case」上「优化模型」比「SFT模型」的生成概率变高,同时「bad case」上「优化模型」比「SFT模型」的生成概率变低。说明生成goodcase概率提升,同时生成badcase概率降低,模型整体变好。
(2) 左边变小,右边更小: 「good case」上「优化模型」比「SFT模型」的生成概率变低,同时「bad case」上「优化模型」比「SFT模型」的生成概率变得更低。说明生成badcase概率大幅降低,模型整体变好。
(3) 左边更大,右边变大: 「good case」上「优化模型」比「SFT模型」的生成概率变得更高,同时「bad case」上「优化模型」比「SFT模型」的生成概率变低。说明生成goodcase概率大幅提升,模型整体变好。
基于偏好数据直接对模型进行微调即可,如果偏好数据正常,一般都是能稳定收敛的。可以观测到margin变大,acurracy提升。
可以看到上图的情况属于: 左边变小,右边更小, 生成badcase概率大幅降低,模型整体变好。
不过可以观测到DPO相比PPO阶段的reward模型的acurracy是下降的,DPO效果相比PPO可能会下降,这可能也是大模型还没广泛应用DPO的原因。
]]>优化方法 | 应用模型 |
---|---|
Multi-Query Attention | ChatGLM2, PaLM, Falcon |
Grouped-Query Attention | LLaMa2 |
首先推导在推理环节Multi-head Attention部分的计算量。
FLOPs,floating point operations,表示浮点数运算次数,衡量了计算量的大小。对于矩阵 $A(ab)$ 和 矩阵 $B(bc)$ 相乘的计算量为$2abc$次浮点数 (乘法 + 加法)
所以在Multi-head Attention部分,假设输入为 [b,s,h],(batch size, max seq len, hidden size),首先Q,K,V分别进行线性层计算,计算量为 $3*(2bshh) = 6bsh^2$。
然后QK进行Multi-head Attnetion运算,假设head num=k,计算的两个矩阵为 $[b,k,s,h/k], [b,k,h/k,s]$,计算量为 $bk2s*(h/k)s = 2bhs^2$,
继续计算在V上的加权,计算量依然为$bk2s*(h/k)s = 2bhs^2$,Attention最后还有一个线性层,计算的两个矩阵为 $[b,s,h], [h,h]$,计算量为 $b2sh*h = 2bsh^2$
所以,整体的计算量为 $6bsh^2 + 2bhs^2 + 2bhs^2 + 2bsh^2 = 8bsh^2 + 4bhs^2$ 当h>>s时候,计算量等效于$O(bsh^2)$
继续推导在推理环节Multi-head Attention部分的显存访问,包括Q,K,V,O这些输入输出为$O(bsh)$,输出的socre矩阵为$O(bhs^2)$,参数矩阵为$O(h^2)$,所以整体的显存占用为$O(bsh+bhs^2+h^2)$
现代GPU的计算量远大于带宽量,模型推理时候的吞吐太低,主要因为带宽不足,所以需要削减显存访问的次数,观察上式,最可能裁剪的就是Attention部分的参数。
Multi-Head Attention,包含了多个Head,其中每个Head又包括了Query,Key,Value三个参数矩阵。
MQA让所有的Head之间 共享同一份Key和Value参数,每个头只单独保留了一份Query参数,从而大大减少Key和Value矩阵的参数量。
实验表明,在推理阶段,beam size=1的时候解码速度能提升约12倍,beam size=4时候能提升约6倍。
这种方式使得模型的参数大幅下降,所以模型的效果相比MHA也会略有损失。
GQA[2]中提出,通过继续训练的方法,可以将一个已经训练好的MHA的模型转换成MQA的模型。大幅节省现有模型升级的成本。
具体分成两个步骤: (1) 权重转换 (2) 继续预训练让模型适应新的结构
权重转换: 将原有的K和V的参数矩阵,通过mean pool的方式合并成一个参数。这种方法相比随机初始化,随机选一个,效果都好更好。具体如下图所示:
继续预训练: 实验表明,继续训练原先5%的语料就能带来巨大的提升,同时当超过10%的语料后收益就会下降。
Grouped-Query Attention(GQA)[2]是MQA和MHA的折中方案。从而实现相比MHA速度更快,同时性能相比MQA更好。 LLaMA2就是采用了GQA的方法。
将query的头分成多个组,每个组共享同一个K,V的参数矩阵。具体如下所示:
实现结果也表明,确实实现了性能和速度的平衡。当G=8的时候,效果大幅领先MQA,同时速度大幅优于MHA并于MQA接近。
当G在1-8之间,推理速度都基本与MQA相近。
类似MQA中的升级思路,将原先的Multi-Head的参数进行分组,每组取均值,构成新的参数。并且继续进行5%-10%的预训练,让模型适应新的模型结构。
[1]Fast Transformer Decoding: One Write-Head is All You Need
[2]GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints
无论是BatchNorm还是LayerNorm,所做都是“减均值,除标准差”的操作,即两种归一化方法都是将数据转换为标准正态分布。
BatchNorm将不同样本相同维度的特征处理为相同的分布。以上述方法对一维数据进行归一化的操作非常常见。假设我们以身高和体重两个指标刻画一个人,即特征维度为2,我们采集了20个人的身高和体重数据,即样本数为20。要知道身高数据的表示可能是175cm、182cm等,而体重数据往往形如63kg、72kg…很明显,从数值来看,身高和体重数据是“风马牛不相及”的两个分布,但是经过上述BatchNorm的加工,二者分布均成为标准正态分布。
BatchNorm针对同一特征,以跨样本的方式开展归一化,因此不会破坏不同样本同一特征之间的关系,毕竟“减均值,除标准差”就是一个平移加缩放的线性操作。在“身高体重”的例子中,这就意味着“归一化前是高个儿的归一化后仍然是高个儿,归一化前胖的归一化后也不会变瘦”。这一性质进而决定了经过归一化操作后,样本之间仍然具有可比较性。但是,特征与特征之间的不再具有可比较性。
BatchNorm认为相同维的特征具有相同分布,因此在特征维度上开展归一化操作,归一化的结果保持样本之间的可比较性。
LayerNorm的归一化方式——计算一个句子的均值和标准差,然后对句中的每个词做归一化操作。
LayerNorm所做的操作,类似于在一个句子中找到一个“语义中心”,然后将句子中的所有词都聚集在这个中心的周围,而句中词与词之间的比较关系不会遭到破坏。
而LayerNorm认为每个样本内的特征具有相同分布,因此针对每一个样本进行归一化处理,保持相同样本内部不同对象的可比较性。
BatchNorm需要平衡小批次统计量和整体样本统计量之间的关系,还需要考虑利用批次统计量更新全局统计量的方法,这也涉及训练和测试阶段使用的统计量有“批次版”和“全局版”的问题…等等。而这些问题到了LayerNorm就都不再是问题——LayerNorm的归一化操作只在样本内部独立开展,因此实际可以完全忽略批次的存在。因此也不用考虑保存和更新的问题且训练和测试应用模式完全一致,均值和标准差随算随用。
为什么Transformer要用LayerNorm? - 铁心核桃的回答 - 知乎
https://www.zhihu.com/question/487766088/answer/3094052709
RMSNorm相比LayerNorm的好处
效果相当,速度更快
相比传统的激活函数,效果更好
引入了额外的参数
]]>编码类型 | 经典模型 |
---|---|
Learned Positional Embedding | BERT, Roberta, GPT, GPT2 |
Sinusoidal | Transformer, Transformer XL |
RoPE | RoFormer, GPT-Neo, LLaMA, ChatGLM, Baichuan7B |
ALiBi | BLOOM, BloombergGPT, Baichuan13B |
通过可学习的Positional Embedding来编码位置信息,是预训练语言模型中最广泛的编码方式。BERT就是这种方式,后续的一系列工作Roberta,GPT2等也都是采用这种方式。
这种方案直接对不同的位置随机初始化一个postion embedding,然后与word embedding相加后输入模型。postion embedding作为模型参数的一部分,在训练过程中进行更新。如下图所示:
这种方案的实现最为简单,只需要初始化一个可学习的position embedding即可,纬度为(最大可编码的长度,隐层大小)。
1 |
|
对BERT的position embedding进行可视化分析,具体如下:
可以看到开头和最后的位置,即0和511独立在一个角落。剩下的位置(1-510)都会连在一起,构成一个不断的线条。这表明每个位置都会被区分开,并且相邻的位置比较接近。
当然这个方案的问题也非常明显,不具备外推的性质。长度在预设定好之后就被固定了。
基于Sinusoidal的位置编码是最初Tranformer文章(Attention is All You Need)中的方案。具体计算方式如下所示:
通过sin和cos函数直接给出了每个位置每个维度上的绝对数值,可以认为直接初始化并固定了position embedding。
作者对比了学习式的position Embedding,结论是与Sinusoidal的效果相近,但是Sinusoidal有更好的外推性,无需设定最大长度max_position
。
1 |
|
最初的Transformer中采用的是Sinusoidal编码,但是BERT以及后续的预训练模型都采用Position Embedding。对比分析可能有下面一些原因:
分析周期可以发现,维度i的周期为 $N^{(i/d)} * 2\Pi$,这里$ 0 <= i < d$,所以周期的范围是 $2\Pi$ 到 $N* 2\Pi$ 。
固定维度d为500,绘制不同N下的position embedding,具体如下:
可以看到随着N的增大,周期会明显变长。文章中的N为10000,作者没有具体的解释,猜测可能是为了能让周期是一个很大的数,更好的区分开每个位置。
Sinusoidal可以学习到相对位置,对于固定位置距离的k,PE(i+k)可以表示成PE(i)的线性函数。证明如下:
通过三角公式进行展开,其中u,v为关于k的常数,所以可以证明PE(i+k)可以由PE(i)的线性表示。
Attention中的重要操作就是点积。计算两个位置的点积 $PE(i+k)PE(i)$ :
通过三角公式进行展开,最终的结果是关于k的一个常数。这表明两个位置向量的点积只和位置差k有关,可以表示两个位置的相对距离。
通过上面的计算很容易可得 $PE(t+k)PE(k) = PE(t)PE(t-k)$,这表明Sinusoidal的编码具有对称性。
同时可以可以发现,随着k的增加,点积的结果会直接减少,即会存在远程衰减。 如下图所示:
但是在实际的Attention计算中还需要与attention的权重 $W$ 进行相乘,即 $ P(t)^TW_{Q}^{T}P(t+k)W_{K} $,这时候点积的结果就不能反映距离了。如下所示:
具体可以参见复旦大学对Sinusoidal编码的分析[1]。
RoPE旋转式位置编码是苏神在RoFormer[2]中提出的,也是目前大模型广泛采用的一种位置编码方式。这种编码不是作用在embedding的输入层,而是作用在与Attention的计算中。
位置m的位置权重 $R_{m}$ 是一个二维的矩阵,具体如下所示:
与位置m的词向量x相乘就是对x注入了位置信息,因为 $R_{m}$ 的矩阵性质,相乘操作可以转换成两个相加的操作:
如果按照维度两两一组拆分来看,相当于对相邻的两个维度(x1,x2)施加了一个旋转的操作,所以也称为旋转式编码:
所以 RoPE 的 self-attention 操作的流程是,对于 token 序列中的每个词嵌入向量,首先计算其对应的 query 和 key 向量,再对每个 token 位置都计算对应的旋转矩阵,接着对每个 token 位置的 query 和 key 向量的元素按照 两两一组 应用旋转变换,最后再计算 query 和 key 之间的内积得到 self-attention 的计算结果。
https://discuss.huggingface.co/t/is-llama-rotary-embedding-implementation-correct/44509
可直接参考huggingface中LLaMA的实现,包含了cache的加速:
1 |
|
苏神自己写的动机是根据欧拉公式的性质,如下所示,相乘后可以表达成差的形式,所以可能可以实现相对位置编码。
先假设现在只有2维,则位置m处的token可以表示成2维的向量q,可以用一个复数坐标系下进行计算:
进一步定义函数f,为query增加位置编码的函数,这里直接相乘 $e^{im\theta}$,如下:
可以看到就相当于对q施加了 $m\theta$ 角的旋转。
可以进一步证明,两个不同的位置m,n,施加位置编码之后的内积只和相对位置(m-n)有关:
只要两两分组即可,并且每组的 $\theta$ 是不同的,就可以推广到d维的场景。
也可以进一步证明,随着相对位置的增加,点积后的结果会逐渐减少,呈现远端衰减。
引用EleutherAI的观点,相比于Sinusoidal,RoPe是作用于一对位置,而非单个位置;RoPe是乘性而非加性,更符合Attention的计算方式。
Attention with Linear Biases (ALiBi)[3]是一种专门提高transformer模型外推能力的编码,让模型在训练阶段的上下文不是很长,但是在预测阶段可以支持很长的上下文。
具体而言,在attention计算之后,会添加一个固定的偏置项,这个偏置项是固定的不需学习的,只跟相对位置有关。如下所示:
对于attention score会根据距离添加“惩罚”,距离越大,惩罚也越大。不同的attention head施加的惩罚系数m是不同。具体而言,假设一共有n个head,系数m是一个等比数列,始于 $2^{-8/n}$,终于 $2^{-8}$。
这是作者实验之后得倒的经验结论。
这种方式的计算和显存占用也都是最优的,因为不需要给query和key都施加位置编码,只需要在内积之后添加一个偏置即可。
可以参考huggingface中BLOOM中的实现
1 |
|
固定训练阶段的最大长度是512或者1024,在预测阶段,ALiBi位置编码可以保持非常稳定的外推性。如下所示:
如果从零设计一个大模型,最佳实践是用什么position encoding其实还没有定论,Rotary和ALiBi目前看都是可尝试的对象。
[1] TENER: Adapting Transformer Encoder for Named Entity Recognition
[2] RoFormer: Enhanced Transformer with Rotary Position Embedding
[3] TRAIN SHORT, TEST LONG: ATTENTION WITH LINEARBIASES ENABLES INPUT LENGTH EXTRAPOLATION
[2] (知乎)为什么BERT不使用Sinusoidal:https://www.zhihu.com/question/307293465
本文将系统介绍如何做一个垂直领域的大模型,包括继续预训练,领域微调数据构建,减缓幻觉,知识召回多个方面。也会介绍整体的系统设计,并串讲一系列相关的论文和产品。
环节 | 方法 |
---|---|
继续预训练 | mixed data, hybrid-tuning |
微调数据构建 | Self-Instruct, Self-QA, Self-KG |
减少幻觉 | Generate with Citation, Factual Consistency Evaluation |
知识召回 | DPR, GTR, Keyword LLM, Context Rewriting, Knowledge Selection |
「你会为一个闲聊的玩具买单吗?」
虽然2023年以来几乎很多公司都发出了自己的通用大模型,但是都还停留在“开放闲聊”阶段,这种泛娱乐的方式是不能带来实际生产力的。所以,以“开放闲聊”为产品形态的ChatGPT,“尝鲜“的流量在6月达到巅峰之后,就开始了出现下滑。
「大模型不能只会开放闲聊」,人们需要的是能帮助我们实实在在解决问题,能提高生产力和工作效率的工具。
例如我们需要一个能帮助写SQL的大模型,这个模型能跟专业的数据工程师一样,准确地给出可信赖的SQL语句,让人们放心的在生产环境执行。如果模型没理解人们的意图,或者不会写,也能进行拒识,而不是“强行”给出一个错误的SQL。
这就要求大模型能忠实于领域内的要求,同时克服“幻觉”,严谨准确地进行作答。当下作为通才的通用大模型很难有这样的能力。
基于上面的思考,开始涌现出越来越多的垂域大模型,这些模型只针对一个特定的领域,甚至只能针对一两个问答的场景。但是已经能初步的产品化落地了,不再是一个只会「闲聊的玩具」,开始真的帮人们解决问题。
下面是一些垂直领域大模型产品化的例子:
法律大模型
法律大模型具备提供基础的法律咨询,完成简单的法律专业文书写作等功能。
https://github.com/PKU-YuanGroup/ChatLaw (北京大学)
医疗大模型
医疗大模型能给人们进行问诊,并支持多模态的输入。
https://www.jiuyangongshe.com/a/dvb0030135 (医联)
教育大模型
多邻国的教育大模型能提供语言学习上的支持,例如答案解析,学习内容规划等。
https://blog.duolingo.com/duolingo-max/ (多邻国)
金融大模型
金融领域大模型数量众多,基本的应用场景也围绕金融的日常工作,例如研报解读等。
参考通用的大模型的训练流程,可以得出垂直领域大模型的基本套路。
需要注意的是一般垂直领域大模型不会直接让模型生成答案,而是跟先检索相关的知识,然后基于召回的知识进行回答。这种方式能减少模型的幻觉,保证答案的时效性,还能快速干预模型对特定问题的答案。
所以SFT和RLHF阶段主要要培养模型的三个能力:
(1) 领域内问题的判别能力,对领域外的问题需要能拒识
(2) 基于召回的知识回答问题的能力
(3) 领域内风格对齐的能力,例如什么问题要简短回答什么问题要翔实回答,以及措辞风格要与领域内的专业人士对齐
下面本文将从预训练,领域微调数据构建,减少幻觉,知识召回四个方面进行具体的介绍。
通过继续预训练能给通用的大模型注入领域知识,领域内的专业词能更充分的学习。这部分只需要准备领域内的语料即可,然后进行LLM任务的继续训练。
科技大模型Mozi[1]在arXiv的4B语料上进行了继续的预训练。可以看到预训练之后在领域内数据上的PPL会明显减少(6.95 -> 3.46),同时在下游任务上相比没有预训练也有明显的提升(0.38 -> 0.52)。
如果想要领域的模型还具备一定的通用能力,通用的能力不会退化(或者灾难性遗忘)这就需要在语言模型训练的时候混杂通用的数据。
例如度小满提出的XuanYuan[2]金融领域大模型,在Bloom基础上进行继续预训练。训练数据包括了通用数据以及领域内的数据。
值得注意的是XuanYuan在训练的过程中还采用了hybrid-tuning的策略,即将预训练的数据(通用+金融领域)以及指令微调的数据(通用+金融领域)混合一起进行训练,而不是拆分成继续训练+指令微调两个阶段,这样模型能很好回答金融领域的问题也能保持对一般问题的作答(论文中的说法,还没有对比实验支撑)。
如果足够有实力也是可以不基于通用大模型进行二次预训练的,而是直接从0训练一个领域大模型,金融大模型BloombergGPT[3]就是这么做的。如果从0进行训练,那就一定要混合通用语料了,能让模型学习到基本的语言语法,世界常识。可以看到BloombergGPT的训练数据中有近一半的数据(48.73%)都是通用领域的数据。
个人观点是垂域大模型可以不用从零开始训练。
回顾人对知识的理解:小学中学都在学习通用领域的知识,然后大学阶段继续进一步学习特定领域的知识。所以在通用模型的基础上继续二次预训练注入领域知识是合理的。
但是如果想通过二次预训练进行语言层面的迁移就会比较难,没有从零开始训练好。回顾人对语言的学习,如果刚“出生”时候就在学习一门语言,进行听说读写的训练,这就是母语了。会比长大以后再去学习一门外语要容易的多,效果也要好很多。
领域微调的核心是构建高质量大规模的领域微调数据。 让人去收集一个领域内的语料是容易的,但是让人去编写领域内的微调指令和回答是很难的。
下面介绍的方法都是来尝试解决这个问题。这些方法的核心都是基于一些已有的数据+GPT4,然后生成领域内的微调数据。
数据生成方法 | 已有数据 | 生成数据 |
---|---|---|
Self-Instruct | 一些单轮/多轮的种子数据 | 单轮/多轮指令微调数据 |
Self-QA | 文档数据 | 单轮指令微调数据 |
Self-KG | 知识图谱 | 单轮指令微调数据 |
Self-Instruct[4]是一种微调数据扩充的方法。如果已经一些种子微调数据(大约100条),可以通过Self-Instruct+GPT4进行扩充,生成更多相对符合要求的微调数据。
Self-Instruct整体流程如下:
一条微调数据包括三个部分:指令,输入 和 输出。下面具体介绍如何生成这三个部分。
首先从种子指令中随机选择一些指令,然后让GPT4参考这些指令,生成一系列类似的指令。
有了指令之后,再让GPT4判断这个指令是一个“分类”问题还是一个“生成”问题。后面会采用不同的答案生成策略。
如果一个问题是“分类”问题,则采用“output-first”的生成方式,即首先生成输出(具体哪个类别),然后再根据指令和输出,生成输入。
例如指令是:”判断下面句子的情感是消极还是积极”,首先生成输出的类别:“积极”,然后再根据指令和类别生成输入的句子:“我今天很开心”。
如果一个问题是“生成”问题,则采用“input-first”的生成方式,即首先生成输入,然后再根据指令和输入,生成输出。
例如指令是:“将下面的句子翻译成英文”,首先生成输入的句子:“我今天很开心”,然后再根据指令和输入生成输出的答案:“I am happy today”。如果一个指令不需要输入的句子,则输入为空。例如指令:“有哪些减肥的运动?”
经过上面的步骤就能初步获得一批微调数据,还需要进行进一步的过滤。例如过滤与已有数据相似度很高的结果,过滤明显低质的结果(指令过长或者过短)。
过滤后的微调数据就可以继续加入“种子指令”中,以此循环,源源不断地进行生成。
如上图所示,分析表明,通过这种方案自动生成的指令数据:
以上是这种方法生成的可用数据。
实验表明,通过在生成的指令上对GPT3进行微调(通过OpenAI的微调API),可以让GPT3达到逼近InstructGPT的效果,如果进一步混合标注的微调数据集则可以超过InstructGPT效果。
另外,生成的指令数据数量越大效果越好,用于数据生成的底座模型越强大效果也会越好。
对Self-Instruct中的prompt进行调整,也可以基于Self-Instruct生成多轮的对话数据进行微调。 例如Mozi[1]中的做法:
如果连基础的种子指令数据都没有,那就不适于Self-Instruct的方法了。这时候可以尝试Self—QA[5]的方法,直接从文档中生成指令数据。整体的流程如下:
基本的思想是:首先根据无结构的文档通过GPT4生成可能的指令,然后输入指令和对应的文档再让GPT4生成问题的答案。
这里的文档可以直接就是文档语料,也可以从结构的表格数据或者图谱数据中生成无结构的文档数据。
基于设计的Prompt就可以让GPT4分别进行指令和答案的生成,由此构成指令微调数据。这些数据还需要进一步通过启发式和规则的方法进行过滤,来提高数据的质量。
上图是一个具体的例子。基于上述微调的数据对模型进行微调,可以让模型在特定的场景上优于通用模型。
如果一个领域已经有了高质量的知识图谱,也可以直接基于知识图谱生成指令数据。这种基于知识的指令数据生成方法是HuaTuo[6]中提出的,本文称为Self—KG。
具体而言,首先需要有一个知识图谱,如下图所示,包括节点和属性关系。
然后从知识图谱中采样一条知识,包括这个知识的全部属性,再设计prompt让GPT4基于这则知识生成指令数据。
这里我们将探讨如何让大模型的生成结果减缓幻觉,同时如何检测幻觉,并在后处理阶段进行消除。
研究“Enabling Large Language Models to Generate Text with Citations”[6]中显示:通过给大模型相关的知识进行参考,并且让模型在生成中附上引用的标注,能提升模型的回答质量,减少幻觉。
让模型输出引用还有一个好处:用户自己可以通过提供的参考快速判断回答对不对(参考不能太长)。 这样即使回答错了,用户也能自己知道,相对可控。
作者的分析表明,回答的质量与召回文档的质量有很大关系,这部分还有很大的提升空间。如何提升知识召回的质量我们在后面会重点分析。
如前文所说,用户可以根据提供的参考快速判断回答是否正确。我们也可以直接训练一个模型来做这样的判断。如果幻觉检测模型判断生成的内容与参考相矛盾,就可以在后处理的阶段对回答进行二次处理。
这个任务叫:事实一致性评估(Factual Consistency Evaluation)[7],属于自然语言推理任务Natural Language Inference(NLI)的一种。具体是给定一个前提知识和一个猜想,判断这个猜想与前提知识的关系,是包含,无关,还是矛盾。
如上图所示,事实一致性评估在很多NLP任务上都有用,包括摘要,转写,基于知识的问答等。所以基于这个任务的数据集(例如Adversarial NLI),可以训练一个评估的模型(例如T5),从而实现对大模型生成的内容进行评估,检测是否在“胡说”。
为了减少回答的幻觉,保证时效性,会先召回相关的知识帮助模型进行回答。Langchain中对这部分有很多实现,包括基于关键词的字面召回,基于相似度模型的语义召回等。
但是实际落地就会发现召回的质量往往较差,下面介绍一些具体的优化方案。
这里根据问题召回相关的文档,本质不是一个相似句子召回问题,因为文档中的回答跟答案的相似度可能是很低的。
所以这里应该建模成Dense Passage Retrieval(DPR)[8]问题,即根据问题召回能回答问题的相关文档。
如图所示,在DPR是一个双塔结构,会有两个独立的编码器分别对问题和文档进行编码。在训练的时候是一个对比学习的loss,即让不相关文档的点积近可能为0,相关文章的点积近可能为1。
科技大模型Mozi[1]就是在科技领域的DPR监督数据上训练一个DPR模型进行知识的召回。
GeneralizableT5-based dense Retrievers(GTR)[9]是相对DPR效果更好的方法。
如图所示,直接采用T5对问题和文章进行编码,同样也是对比学习的loss。需要注意的是这里问题和文章是同一个编码器。
进一步分析可以发现,随着模型尺寸的增大效果也会越来越好,这种方法也优于DPR。当然因为参数量更大了,推理速度也要比DPR更慢。
在专业的垂直领域,待检索的文档都是非常专业的表述,而用户的问题往往是非常不专业的白话表达。所以直接拿用户的query去检索,召回的效果就会比较差。Keyword LLM就是解决这其中GAP的。
例如在ChatDoctor[10]中(下图),会先让大模型基于用户的query生成一系列的关键词,然后再用关键词去知识库中做检索。ChatDoctor是直接用In-Context Learning的方式进行关键词的生成。
我们也可以对大模型在这个任务上进行微调,训练一个专门根据用户问题生成关键词的大模型。这就是ChatLaw[11]中的方案(下图)。
再进一步思考多轮的场景,如果直接拿用户当前的问题去检索就会面临信息缺失的问题。 例如:
1 |
|
此时直接拿“那里有哪些景点?”去做检索,肯定会召回很多无关的内容。用户真实的问题应该是:“北京有哪些旅游景点?”。
所以在多轮的场景中做知识召回需要先整合当前问题和对话历史,然后对当前的问题进行改写,使其能成为一个“独立问题”,然后再用改写后的“独立问题”进行知识检索。这就是Context Rewriting。
这个任务有一些现成的数据集,例如下面的Restoration200K[12]:
对于多轮场景下的完整问答任务(问题改写+知识召回)也有相关的数据集,例如下面的Orca,标注了每个问题需要召回的文档以及基于文档的回答。
还需要思考需要召回多少个文档,如何过滤,如何排序?
Mozi[1]中的实验表明,一个问题的回答,往往分布在多个段落中,更多的召回有助于给出更综合的回答。但是随着召回文章的上升,回答的召回率提升,但是不相关的片段也更多,会导致精确率下降。召回3-4个文档是较优的折中方案。
同样我们也可以利用大模型对召回的结果进行二次的精排(过滤)。例如下图ChatDoctor[9]中的方案,编写prompt让模型在召回的文档中选择对回答问题有帮助的文档。
对于召回文档的排序,一个经验的方案是相关性更高的文档离问题的位置更近。
基于上面的讨论,垂直领域的大模型需要基于检索系统进行构建,不是一个单单的大模型而是一整个系统。
下面是阿里云提出的系统,包括问题解析,知识召回和推理求解三个模块:
下面是ChatLaw[10]的系统架构,包括Kyeword LLM 和 ChatLaw LLM两个大模型和一个召回模块。
但这也还是不够,距离能产品化还有很大的距离,还有很多的corner case没有解决,例如:
“道路是曲折的,前途是光明的”。大模型的产品化落地一定会有很多的挑战,但是也相信会被一个个地解决。期待大模型超级应用的出现!
分词方法 | 典型模型 |
---|---|
BPE | GPT, GPT-2, GPT-J, GPT-Neo, RoBERTa, BART, LLaMA, ChatGLM-6B, Baichuan |
WordPiece | BERT, DistilBERT,MobileBERT |
Unigram | AlBERT, T5, mBART, XLNet |
基于subword的切分能很好平衡基于词切分和基于字切分的优缺点,也是目前主流最主流的切分方式。
但是基于词和字的切分都会存在一定的问题,直接应用的效果比较差。
基于词的切分,会造成:
基于字的切分,会造成:
所以基于词和基于字的切分方式是两个极端,其优缺点也是互补的。而折中的subword就是一种相对平衡的方案。
subword的基本切分原则是:
基于subword的切分可以实现:
基于subword的切分包括:BPE,WordPiece 和 Unigram 三种分词模型。
Tokenizer包括训练和推理两个环节。训练阶段指得是从语料中获取一个分词器模型。推理阶段指的是给定一个句子,基于分词模型切分成一连串的token。
基本的流程如图所示,包括归一化,预分词,基于分词模型的切分,后处理4个步骤。
这是最基础的文本清洗,包括删除多余的换行和空格,转小写,移除音调等。例如:
1 |
|
HuggingFace tokenizer的实现: https://huggingface.co/docs/tokenizers/api/normalizers
预分词阶段会把句子切分成更小的“词”单元。可以基于空格或者标点进行切分。 不同的tokenizer的实现细节是不一样的。例如:
1 |
|
可以看到BERT的tokenizer就是直接基于空格和标点进行切分。
GPT2也是基于空格和标签,但是空格会保留成特殊字符“Ġ”。
T5则只基于空格进行切分,标点不会切分。并且空格会保留成特殊字符”▁”,并且句子开头也会添加特殊字符”▁”。
HuggingFace tokenizer的实现: https://huggingface.co/docs/tokenizers/api/pre-tokenizers
这里指的就是不同分词模型具体的切分方式。分词模型包括:BPE,WordPiece 和 Unigram 三种分词模型。
HuggingFace tokenizer的实现: https://huggingface.co/docs/tokenizers/api/models
后处理阶段会包括一些特殊的分词逻辑,例如添加sepcial token:[CLS],[SEP]等。
HuggingFace tokenizer的实现: https://huggingface.co/docs/tokenizers/api/post-processors
Byte-Pair Encoding(BPE)是最广泛采用的subword分词器。
在训练环节,目标是给定语料,通过训练算法,生成合并规则和词表。
BPE算法是从一个字符级别的词表为基础,合并pair并添加到词表中,逐步形成大词表。合并规则为选择相邻pair词频最大的进行合并。
下面我们进行手工的实现。
假定训练的语料(已归一化处理)为4个句子。
1 |
|
首先进行预切分处理。这里采用gpt2的预切分逻辑。
具体会按照空格和标点进行切分,并且空格会保留成特殊的字符“Ġ”。
1 |
|
获得的pre_tokenized_corpus如下,每个单元分别为[word, (start_index, end_index)]
1 |
|
进一步统计每个整词的词频
1 |
|
获得word2count如下
1 |
|
因为BPE是从字符级别的小词表,逐步合并成大词表,所以需要先获得字符级别的小词表。
1 |
|
获得的初始小词表vocabs如下:
1 |
|
基于小词表就可以对每个整词进行切分
1 |
|
1 |
|
基于word2splits统计vocabs中相邻两个pair的词频pair2count
1 |
|
获得pair2count如下:
1 |
|
统计当前频率最高的相邻pair
1 |
|
经过统计,当前频率最高的pair为: (‘Ġ’, ‘t’), 频率为7次。
将(‘Ġ’, ‘t’)合并成一个词并添加到词表中。同时在合并规则中添加(‘Ġ’, ‘t’)这条合并规则。
1 |
|
此时的vocab词表更新成:
1 |
|
根据更新后的vocab重新对word2count进行切分。具体实现上,可以直接在旧的word2split上应用新的合并规则(‘Ġ’, ‘t’)
1 |
|
从而获得新的word2split
1 |
|
可以看到新的word2split中已经包含了新的词”Ġt”。
重复上述循环直到整个词表的大小达到预先设定的词表大小。
1 |
|
假定最终词表的大小为50,经过上述迭代后我们获得的词表和合并规则如下:
1 |
|
至此我们就根据给定的语料完成了BPE分词器的训练。
在推理阶段,给定一个句子,我们需要将其切分成一个token的序列。
具体实现上需要先对句子进行预分词并切分成字符级别的序列,然后根据合并规则进行合并。
1 |
|
例如
1 |
|
2019年提出的Byte-level BPE (BBPE)算法是上面BPE算法的进一步升级。具体参见:Neural Machine Translation with Byte-Level Subwords。
核心思想是用byte来构建最基础的词表而不是字符。首先将文本按照UTF-8进行编码,每个字符在UTF-8的表示中占据1-4个byte。
在byte序列上再使用BPE算法,进行byte level的相邻合并。编码形式如下图所示:
通过这种方式可以更好的处理跨语言和不常见字符的特殊问题(例如,颜文字),相比传统的BPE更节省词表空间(同等词表大小效果更好),每个token也能获得更充分的训练。
但是在解码阶段,一个byte序列可能解码后不是一个合法的字符序列,这里需要采用动态规划的算法进行解码,使其能解码出尽可能多的合法字符。具体算法如下:
假定$f(k)$表示字符序列$B_{1,k}$最大能解码的合法字符数量,$f(k)$有最优的子结构:
$f(k) = max_{t=1,2,3,4}{f(k-t) + g(k-t+1, k)}$
这里如果$B_{i,j}$为一个合法字符$g(i,j) = 1$,否则$g(i,j) = 0$。
WordPiece分词与BPE非常类似,只是在训练阶段合并pair的策略不是pair的频率而是互信息。
$socre = log(p(ab)) - (log(p(a)) + log(p(b))) = log(p(ab)/p(a)p(b))$
这里的动机是一个pair的频率很高,但是其中pair的一部分的频率更高,这时候不一定需要进行该pair的合并。
而如果一个pair的频率很高,并且这个pair的两个部分都是只出现在这个pair中,就说明这个pair很值得合并。
在训练环节,给定语料,通过训练算法,生成最终的词表。
WordPiece算法也是从一个字符级别的词表为基础,逐步扩充成大词表。合并规则为选择相邻pair互信息最大的进行合并。
下面进行具体手工实现。
假定训练的语料(已归一化处理)为
1 |
|
首先进行预切分处理。这里采用BERT的预切分逻辑。具体会按照空格和标点进行切分。
1 |
|
获得的pre_tokenized_corpus如下,每个单元分别为[word, (start_index, end_index)]
1 |
|
进一步统计词频
1 |
|
获得word2count如下
1 |
|
因为WordPiece同样是从字符级别的小词表,逐步合并成大词表,所以先获得字符级别的小词表。注意这里如果字符不是不一个词的开始,需要添加上特殊字符”##”。
1 |
|
获得的初始小词表vocabs如下:
1 |
|
基于小词表对每个词进行切分
1 |
|
1 |
|
进一步统计vocabs中相邻两个pair的互信息
1 |
|
获得每个pair的互信息如下:
1 |
|
统计出互信息最高的相邻pair
1 |
|
此时互信息最高的pair为: (‘a’, ‘##b’)
将(‘a’, ‘##b’)合并成一个词’ab’并添加到词表中
1 |
|
这样vocab词表更新成:
1 |
|
根据更新的vocab重新对word2count进行切分。
1 |
|
获得新的word2split
1 |
|
可以看到新的word2split中已经包含了新的词”ab”。
重复上述步骤,直到整个词表的大小达到预先设定的词表大小。
1 |
|
假定最终词表的大小为70,经过上述迭代后我们获得的词表如下:
1 |
|
注意词表中添加了特殊的token:[CLS], [MASK], [PAD], [SEP], [UNK]
至此我们就根据给定的语料完成了WordPiece分词器的训练。
在推理阶段,给定一个句子,需要将其切分成一个token的序列。
具体实现上需要先对句子进行预分词,然后对每个词进行在词表中进行最大前向的匹配。如果词表中不存在则为UNK。
1 |
|
例如
1 |
|
Unigram分词与BPE和WordPiece不同,是基于一个大词表逐步裁剪成一个小词表。
通过Unigram语言模型计算删除不同subword造成的损失来衡量subword的重要性,保留重要性较高的子词。
在训练环节,目标是给定语料,通过训练算法,生成最终的词表,并且每个词有自己的概率值。
Unigram算法是从大词表为基础,逐步裁剪成小词表。裁剪规则是根据Unigram语言模型的打分依次裁剪重要度相对较低的词。
下面进行具体手工实现。
假定训练的语料(已归一化处理)为
1 |
|
首先进行预切分处理。这里采用xlnet的预切分逻辑。具体会按照空格进行切分,标点不会切分。并且空格会保留成特殊字符”▁”,句子开头也会添加特殊字符”▁”。
1 |
|
获得的pre_tokenized_corpus如下,每个单元分别为[word, (start_index, end_index)]
1 |
|
进一步统计词频
1 |
|
获得word2count如下
1 |
|
统计词表的全部子词,并统计词频。取前300个词,构成最初的大词表。为了避免OOV,char级别的词均需要保留。
1 |
|
获得的初始小词表vocabs如下:
1 |
|
进一步统计每个子词的概率,并转换成Unigram里的loss贡献
1 |
|
1 |
|
基于每个子词的loss以及Viterbi算法就可以求解出,输入的一个词的最佳分词路径。即整体语言模型的loss最小。词的长度为N,解码的时间复杂度为O(N^2)。
1 |
|
例如:
1 |
|
基于上述的函数,可以获得任一个词的分词路径,以及loss。这样就可以计算整个语料上的loss。
1 |
|
尝试移除model中的一个子词,并计算移除后新的model在全部语料上的loss,从而获得这个子词的score,即删除这个子词使得loss新增的量。
1 |
|
为了提升迭代效率,批量删除前10%的结果,即让整体loss增量最小的前10%的词。(删除这些词对整体loss的影响不大。)
1 |
|
获得新的词表后,重新计算每个词的概率,获得新的模型。并重复以上步骤,直到裁剪到词表大小符合要求。
1 |
|
假定预设的词表的大小为100,经过上述迭代后我们获得词表如下:
1 |
|
在推理阶段,给定一个句子,需要将其切分成一个token的序列。
具体实现上先对句子进行预分词,然后对每个词基于Viterbi算法进行解码。
1 |
|
例如
1 |
|
基于Viterbi的切分获得的是最佳切分,基于unigram可以实现一个句子的多种切分方式,并且可以获得每种切分路径的打分。
SentencePiece是Google出的一个分词工具:
当前主流的大模型都是基于sentencepiece实现,例如ChatGLM的tokenizer。
1 |
|
https://huggingface.co/THUDM/chatglm-6b/blob/main/tokenization_chatglm.py#L21
当SentencePiece在训练BPE的时开启--byte_fallback
, 在效果上类似BBPE,遇到UNK会继续按照byte进行进一步的切分。参见:https://github.com/google/sentencepiece/issues/621
具体实现上是将<0x00> … <0xFF>这256个token添加到词表中。
分析ChatGLM的模型,可以发现ChatGLM就是开启了--byte_fallback
1 |
|
output:
1 |
|
可以看到byte_fallback: true
同样的方法,可以验证LLaMA, ChatGLM-6B, Baichuan这些大模型都是基于sentencepiece实现的BPE的分词算法,并且采用byte回退。
本文将通过10个快问快答,深度解析什么是ChatGPT?背后的技术原理是什么?可以帮我们做好哪些事情?还有哪些局限性?希望本文能帮助你在繁杂的舆论中不迷失,并开始真正利用ChatGPT来提高工作和学习效率!相信看完会有新收获!
ChatGPT是由OpenAI在2022年11月推出的一款AI产品。OpenAI是一家美国人工智能研究实验室,其使命是构建通用人工智能,并造福全人类。Altman是公司的CEO,微软和马斯克是这家公司的投资者。正因为其使命是造福全人类,所以注重AI平等,例如开放的ChatGPT接口即使你再有钱也有速率限制,以保证普通人也能公平的使用到。
可以将ChatGPT类比一个“人”,任何问题都可以试着找ChatGPT给个参考回答。具体而言:可以帮助你进行内容创作(写报告,写作文,写诗,写歌等); 帮助你翻译或者润色论文;帮助你准备考试(批改你写的作文,生成相关习题等);帮助你编写代码/调试程序;帮助你整理会议纪要,制作PPT等等。ChatGPT能做的事情,取决于你如何向ChatGPT进行提问,这里的提问也称之为提示词(prompt)。所以如果对ChatGPT的结果不满意,可以尝试换个提示词写法,也可以先Google对于某个任务更好的提示词应该是什么。
是的。ChatGPT 是一种 GPT (Generative Pre-trained Transformer) 生成式预训练神经网络。该神经网络可以根据输入的文本预测下一个词可能是什么。ChatGPT与其他 GPT 模型(例如 GPT-2、GPT-3 和 GPT-4)有着相同的网络结构,但专门针对「如何根据指令回答问题」以及「如何让回答符合“人类价值”」做了优化。
没有。2018年发布的GPT-1有大约1亿参数,2019年发布的GPT-2有大约19亿参数,2020发布的GPT-3有1750亿参数。虽然拥有千亿参数量的GPT-3是ChatGPT的基础,但是ChatGPT只有约20亿参数。
ChatGPT的基础是GPT-3,GPT-3的单次训练成本高达250万美金。2015年Openai成立开始做GPT的启动资金就高达10亿美金,2019年微软继续投资了10亿美金,2023年在ChatGPT爆火后微软又继续投资了100亿美金。而这些钱的主要用途就是GPT类大模型的研发与训练。
ChatGPT的模型结构与GPT-3一致是语言模型,该模型的输入和输出均为文本,没有记忆功能,所以只能实现单轮的问答。对于多轮的对话,ChatGPT会将之前的问答进行拼接,再继续拼接当前用户的问题,共同构成模型的输入。这时模型就能参考整个之前的对话内容给出回答,从而实现“多轮”。
不能。由于模型的输入和输出长度是有限制的,为2048个token(最新版为4096个token),所以如果历史对话过长就会超过模型能处理的长度限制。而刚推出的GPT-4相比ChatGPT的一个重要优势就是模型输入长度的限制大幅提升到25,000个token。不过即便如此,理论上GPT-4也不能“无限”聊下去。
ChatGPT所拥有的丰富知识储备,来自于它的训练数据,以及足够大的模型参数。这些数据大致可以分为三个大的范畴:网页内容、书籍内容以及百科内容。百科和书籍不必多说,天然蕴含了大量的知识。这里网页内容包含了许多新闻、评论、观点等,并且网页还包括很多高质量的问答垂直类网站,例如知乎,这些都是ChatGPT的知识来源。但是因为训练数据收集截至到2021年,所以对于2021年之后的问题无法回答。
ChatGPT对不同语言的支持程度取决于在训练数据中相关语言的占比。ChatGPT目前支持超过90种语言,支持多语种混合输入,但是训练语料中的主流语言依然是英文(超过90%),中文的占比很小。所以如果对回答不满意,可以试着将问题转换成英文后再与ChatGPT进行交互。
ChatGPT不是完美的还具有很多的缺陷,例如:1. 可能给出错误的回答,并且无法给出回答的来源,可能会一本正经胡说八道; 2. 因为数据截止到2021年,所以最新的知识无法回答;3. 只能输入输出只能是文本,而不能理解/生成图片,视频等富媒体 ;4. 对于逻辑推理,数学计算等效果较差 ;5. 对于输入的提示词(问法)很敏感,例如对于某个问题,无法回答,但是如果重新措辞,可能就会轻松回答。
当然这些问题也已经逐步在解决中,例如集成了ChatGPT的搜索引擎 new being就可以给出答案的来源并且支持从互联网上获取最新的知识;刚推出的GPT-4也已经可以支持图片的输入;同样ChatGPT的插件也可以部分解决数学计算的问题。
ChatGPT目前不支持国内以及香港地区的用户访问。即使成功访问,想真正用上ChatGPT也非常麻烦,需要国外的手机号和信用卡。所以国内有直接通过ChatGPT API搭建的镜像站点。这里推荐一直在用的“一粟创作平台”,可以在国内稳定访问ChatGPT,PC端/移动端都支持,并且在持续集成更多的AI能力,例如AI作画,ChatPDF等。具体参见:https://ai.yisukeyan.com/ 。
可以通过邀请链接直接注册,注册后就能免费体验!
豆瓣链接:https://book.douban.com/subject/35334595/
编程类书籍而非算法类书籍,提供了一种思维逻辑来帮助我们思考当前的问题是什么,有什么约束条件,合理的解决方法有哪些。
豆瓣链接:https://book.douban.com/subject/1949420/
看狂飙而被“种草”的书,阅读下来确实深感古人的智慧,不战而屈人之兵,多用谋略而非蛮力。
豆瓣链接:https://book.douban.com/subject/35659418/
很厚的一本书,系统介绍了芯片的发展历史,老的巨头(英特尔,摩托罗拉,IBM)的衰落,以及新的巨头的崛起(阿斯麦尔,英伟达,超微,台积电),没有人能一直处于领先,中国也还有机会。预感在AI火热的当下,GPU也会产生新的摩尔定律,让GPU的性能与成本持续下降。看好英伟达。
搞机器学习的人一定会绕不开「生成模型」和「判别模型」,但是要理解这两个模型有什么区别还是不容易的
$\sum{P(X,Y)} = 1, P(X,Y) = P(X|Y)P(Y), P(Y|X) = P(X,Y)/P(X)$
对于未见示例X,要求出X与不同标记之间的联合概率分布,对比之后最大的概率为最终的Y
典型的生成模型包括:朴素贝叶斯,隐马尔可夫模型HMM
$\sum{P(Y|X)} = 1$
对未见示例X,根据P(Y|X)可以求得标记Y,即可以直接判别出最终的Y
典型的判别模型包括:线性回归模型、支持向量机SVM, 条件随机场CRF
学生时代常常一个人维护一个Git仓库,所以很是随性,不管分支,永远在master上开发,commit message也随便写,工作之后才意识到Git的重要性。
其实Git的工作流跟一个产品的推进的工作流紧密相关,如何利用Git进行产品研发,产品测试,产品发布,快速修复缺陷,这些都在Git的分支管理中。
master分支
代码库应该有一个、且仅有一个主分支。所有提供给用户使用的正式版本,都在这个主分支上发布。
主分支必须是可用的、稳定的、可直接发布的版本,不能直接在主分支上开发。
dev分支
master主分支只用来发布重大版本,日常开发应该在另一个分支上完成,我们把开发用的分支,叫做dev。这个分支可以用来生成代码的最新隔夜版本(nightly),进行最新功能的体验。
如果想正式对外发布,就在master分支上对dev分支进行合并(merge)。
dev分支代码永远是最新的,所有新功能以这个分支来创建自己的开发分支,该分支只做合并操作,不能直接在该分支上开发。
feature分支
功能分支的名字,可以采用feature-*的形式命名,以自己开发的功能命名。
功能分支是分配开发不同的功能用的,从dev创建功能分支,然后完成相应功能开发后,通过重新整理commit记录,然后合并回dev分支并删除该功能分支。这里采用--no-ff
release预发布分支
预发布分支名字,可以采用release-*的形式命名
预发布分支是指发布正式版本之前(即合并到master分支之前),我们可能需要有一个预发布的版本进行测试,主要是提交测试团队进行测试
预发布分支是从dev分支上分出来的,预发布结束之后(即测试没有问题之后),必须合并进dev和master
release-bug修复预发布分支
修复预发布分支的bug,可以采用release-bug-*的形式命名
在预发布版本测试出现bug时,从release分支创建分支进行bug修复,bug修复完成后在合并会release分支
master-bug紧急修补分支
修补分支的名字,可以采用bugfix-master-*的形式。该分支是为了紧急修复线上的bug。
软件正式发布之后,难免会出现bug。这时就需要创建一个分支,进行bug修补。修补bug分支是从master分支上面分出来的。修补结束之后,将hotfix分支提交测试进行测试,通过后直接合并进master和dev分支。
注:一个分支尽量开发一个功能模块,不要多个功能模块在一个分支上开发. feature分支申请合并之前,最好先pull一下dev分支下来,看一下有没有冲突,如果有冲突就先解决冲突后再合并回dev
如果在小团队也可以精简一下,只有一个master分支,不用dev分支,其他逻辑不变。
]]>随着预训练语言模型的快速发展,很多问题可以通过堆数据和堆模型参数简单粗暴的有效解决。所以亲自训练一个大模型一定是每个NLPer都想尝试的事,这时候就需要进行多机多卡的分布式训练了。本文是一篇踩坑后的总结,介绍如何基于huggingface的transformers库来快速实现。
注意本文仅涉及数据并行,而不涉及模型并行。所以参考本文可以自己从零训练一个bert,bert-large等,但想训练万亿参数的超大模型(一张显卡都不能存储模型的参数)就需要更复杂的实现了。
一般rank编号为0的进程会作为master进程
具体举个例子:当前有2个节点,每个节点有8块GPU卡,然后启动多机多卡的分布式训练用满这16块卡,这时候:
如果通过python -m torch.distributed.launch
的方式启动,部分参数都会自动注入到环境变量中,可以在脚本中进行获取。例如:
1 |
|
训练大模型一定是基于大数据,可能非常大(例如上百GB),所以不能采用map-style的dataset作为训练集的dataset,因为无法直接load到内存中,所以需要采用IterableDataset。同时为了训练的数据较快,需要采用多进程的数据加载,即num_worker>0
。
这时候假设在一个2个节点,每个节点8张卡的分布式环境中,同时采用10个子进程进行数据加载。
那么此时在数据加载阶段启用的进程总数为: 2 * 8 * 10 = 160
在IterableDataset如果直接按照最简单的写法,如下所示:
1 |
|
通过日志打印可以发现,同一份数据将被160个进程重复加载,这显然就不是数据并行了。
所以迭代阶段就需要进行精细的处理,避免一份数据被多个进程重复加载。参考Pytorch官方的文档,可以发现实际上是预计算出每个子进程需要迭代的区间,然后结合子进程的信息找到对应的区间进行迭代。
https://pytorch.org/docs/stable/data.html#torch.utils.data.IterableDataset
1 |
|
但是这里只处理了单块GPU卡多子进程加载数据的写法,我们这里是分布式的多机多卡,所以还需要对以上代码进行改造,进一步引入rank的信息。
1 |
|
通过以上处理后,可以观察到160个进程每个进程加载的数据都是不一样的。
如果采用原生的torch.distributed.launch
进行多机多卡的训练是需要写很多范式代码的,例如init_process_group
。而采用transformers
的Trainer
会自动帮你去适配你当前的环境,也就是无论是单机单卡,还是单机多卡,还是多机多卡都是一份代码,并且对于fp16这种配置也就是一个参数项。最近发现Trainer
实现分布式训练的底层逻辑其实已经进一步抽象成了一个新的库huggingface/accelerate。
贴一段这个库的描述:🚀 A simple way to train and use PyTorch models with multi-GPU, TPU, mixed-precision
再贴一段这个库与transformers
的trainer
的关系:https://github.com/huggingface/accelerate/issues/144
所以直接使用Trainer
就好了!! 下面是一段示例代码:
1 |
|
train.sh
1 |
|