一些LLM总结

最近的一些学习计划

1、自回归和自编码区别?

通俗解释 一文读懂GPT家族和BERT的底层区别——自回归和自编码语言模型详解 - 知乎 (zhihu.com)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
引申的一些思考?
BERT的假设
BERT在MLM任务中,对于被遮盖的token,会尝试根据周围未遮盖的token来预测其内容。在这个过程中,BERT假设每个待预测的token的生成只依赖于其周围的未遮盖token,而不考虑其他待预测token的信息。这意味着,BERT在预测一个token时,不会考虑其他同时被遮盖的token的存在或可能的值。

假设的简化性
这种假设简化了模型的计算复杂性,因为模型不需要考虑所有token之间的复杂交互。这使得BERT能够更高效地进行训练和推理。

假设的局限性
然而,这种假设也过于简单化了自然语言中的实际情况。在自然语言中,token之间往往存在复杂的高阶(high-order)和长距离(long-range)依赖关系。例如:

高阶依赖:一个token的意义可能受到多个其他token的共同影响,而不仅仅是其直接相邻的token。
长距离依赖:某些token之间的关系可能跨越很长的文本距离,例如在一个句子中的某个token可能与另一个句子中的token有重要的关联。
对模型性能的影响
由于BERT的这种简化假设,它在处理某些需要考虑复杂token间依赖的任务时可能会遇到挑战。尽管BERT在许多NLP任务上都取得了显著的性能提升,但在一些需要深入理解长文本或复杂语义结构的任务中,其性能可能不如那些能够更好地捕捉token间复杂依赖关系的模型。

结论
BERT的MLM任务中的假设简化了模型的计算复杂性,但也限制了其在处理复杂token间依赖关系方面的能力。为了克服这些局限性,研究者们提出了各种改进方法,如使用更大的上下文窗口、引入跨层连接、采用更复杂的模型架构等,以增强模型捕捉长距离和高阶依赖的能力。

个人理解:换句话说,双向和单向transformer的区别,感觉就是mask矩阵的区别,双向对应bert,也就是encoder,单向对应gpt,也就是decoder。(如有错误敬请指正)

2、一文读懂「Chain of Thought,CoT」思维链

一文读懂「Chain of Thought,CoT」思维链-CSDN博客

**关于 CoT 为什么会生效,目前尚且没有一套被大家广泛接受的普遍理论。**但是,有许多论文对 CoT 与大模型的互动进行了一系列实验,类似物理实验与物理理论的关系,在实验中一些有意思的现象或许可以帮助我们理解 CoT 的工作原理:
  • 模型规模小会导致 CoT 失效;
  • 简单的任务 CoT 不会对模型性能带来提升;
  • 训练数据内部彼此相互联结程度的增加可以提升 CoT 的性能;
  • 示例中的错误,或者无效的推理步骤不会导致 CoT 性能的下降;
    如果我们对这些现象做一些总结与延申,或许可以认为:首先,CoT 需要大模型具备一些方面“最基础”的知识,如果模型过小则会导致大模型无法理解最基本的“原子知识”,从而也无从谈起进行推理;其次,使用 CoT 可以为一些它理解到的基础知识之间搭起一座桥梁,使得已知信息形成一条“链条”,从而使得大模型不会中途跑偏;最后,CoT 的作用,或许在于强迫模型进行推理,而非教会模型如何完成推理,大模型在完成预训练后就已经具备了推理能力,而 CoT 只是向模型指定了一种输出格式,规范模型让模型逐步生成答案。

什么时候使用CoT?
CoT 应当被用于 20B 以上参数规模的模型之中,并且模型的训练数据应当于任务问题相关且彼此相互有较强的联结。
首从工程的角度而言,CoT 的适用场景抽象一下可以被归纳为三点,分别是使用大模型(1),任务需要复杂推理(2),参数量的增加无法使得模型性能显著提升(3)。此外,现有的论文实验也表明,CoT 更加适合复杂的推理任务,比如计算或编程,不太适用于简单的单项选择、序列标记等任务之中,并且 CoT 并不适用于那些参数量较小的模型(20B以下),在小模型中使用 CoT 非常有可能会造成机器幻觉等等问题。

而从理论角度,一篇来自斯坦福的论文《Why think step-by-step? reasoning emerges from the locality of experience》揭示了**当大模型的训练数据表现出了变量的局部簇结构(Local Clusters of Variables)**时,CoT 将会展现极好的效果。而变量的局部簇主要指训练数据中变量之间有着强的相互作用,相互影响的关系。

此外,也有研究指出,当给予大模型的示例之间彼此之间互相区分并不相同时,也有助于提升 CoT 的性能。同时,逻辑依据是否与问题相关,逻辑推理步骤的顺序也会显著影响 CoT 的性能。另外一个有趣的发现是,使用代码数据训练大模型,或者使用符合 CoT 格式的数据训练模型也有助于提升 CoT 的性能。总结一下:CoT 应当被用于 20B 以上参数规模的模型之中,并且模型的训练数据应当于任务问题相关且彼此相互有较强的联结。

3、prompt tuning到底是个啥?和instruction的区别?

深入浅出Prompt Learning要旨及常用方法 (qq.com)

Prompt Learning 的本质:

将所有下游任务统一成预训练任务;以特定的模板,将下游任务的数据转成自然语言形式,充分挖掘预训练模型本身的能力。

本质上就是设计一个比较契合上游预训练任务的模板,通过模板的设计就是挖掘出上游预训练模型的潜力,让上游的预训练模型在尽量不需要标注数据的情况下比较好的完成下游的任务,关键包括 3 个步骤:

  1. 设计预训练语言模型的任务
  2. 设计输入模板样式(Prompt Engineering)
  3. 设计 label 样式及模型的输出映射到 label 的方式(Answer Engineering)

Prompt Learning 的形式:

以电影评论情感分类任务为例,模型需根据输入句子做二分类:

原始输:特效非常酷炫,我很喜欢。

Prompt 输入:提示模板 1:特效非常酷炫,我很喜欢。这是一部 [MASK] 电影;提示模板 2:特效非常酷炫,我很喜欢。这部电影很 [MASK]

提示模板的作用就在于:将训练数据转成自然语言的形式,并在合适的位置 MASK,以激发预训练模型的能力。

4、Adapter和lora到底是个啥?

前者是纵向深度上加模块,lora是横向加旁路吧,在每个矩阵旁边加旁路

5、旋转位置编码

transformer对于输入token的位置不敏感,比如一句话,经过一层transformer计算以后得到的向量,和你改变这句话的顺序,得到的输出是一样了,只是交换了位置。我们想要的是改变句子顺序的时候,输出也不一样。

原始的transformer就是加入了一个绝对位置编码,Rope就是旋转位置编码。其实我感觉绝对位置编码sin cos也是能够表达相对位置信息的,但是由于sin cos的周期性,长距离依赖没有ROPE好。

6、数据集的构建

比如医疗问答对,像这样的数据集,你可以想好input,然后丢给chatgpt,根据chatgpt的输出来构建output,甚至同时利用多个大模型来回答。再不济,可以直接使用网上别人构建好的。

7、为什么需要二阶段精排

首先embbeding就有信息损失,其次一阶段召回是用一个KNN或者KNN类似的算法去得到一个粗造的结果,是牺牲了精度换取时间,因此我们需要二阶段精排,重新去计算得分。

Bi-encoder:得到输入的一个embbeding表示,然后再计算余弦相似度,速度快。

cross-encoder:主要计算两个文本的相似性。

从训练任务上来看,前者侧重于embbeding,后者侧重于计算文本相似性。实际上一阶段就是Bi-encoder,二阶段就是cross-encoder。两者都是bert的延申。

当然还有其他方面的原因,比如你不精排,n选的越大,但是超过了大模型的上下文长度,也会有问题。

8、triton例子!官方文档写一遍

一文讲明白大模型显存占用(只考虑单卡) - 知乎 (zhihu.com)

Qwen2微调分类的一个例子:Qwen2大模型微调入门实战(完整代码)-CSDN博客

9、不同tokenizer的区别,bpe、bbpe、wordpiece等

BPE:BPE是按照字符划分的,将文本分割成字符序列。每个字符(包括特殊字符和空格)都作为单独的符号。

BBPE: 基于字节的划分和合并, 其余与BPE一样

区别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
BPE (Byte Pair Encoding)

Byte Pair Encoding 是一种分词技术,最早应用于数据压缩。后来的改进版本被使用于自然语言处理任务中,以提高模型在词汇表足够小的情况下处理未见过单词的能力。

工作原理:
1. 初始化:将文本分割成字符序列。每个字符(包括特殊字符和空格)都作为单独的符号。
2. 迭代合并:频繁地查找最常见的相邻字符对,并将它们合并成一个新的符号。这个过程不断迭代,直到达到预设的合并步骤次数或者词汇表大小。

例如,单词 "hello" 可以首次分割为 ["h", "e", "l", "l", "o"],然后通过逐步合并,如 ["h", "e", "ll", "o"],最后变成 ["h", "ello"] 或 ["he", "llo"](具体取决于字符对的频次)。

特点:
- 通过最常见的字符对合并,可以减少词汇量,同时适应新词汇。
- 特别适合处理自然语言中的词变形和新词。


BBPE (Byte-Level BPE)
Byte-Level Byte Pair Encoding 是对传统 BPE 的一种变体,旨在处理字符集有限或者文字表达复杂性的语言,例如 Unicode 字符集或非拉丁字符集。 BBPE 的一个关键目标是使其对于不同语言更加通用,尤其是那些使用不同字母表或字符集的语言。

工作原理:
1. 初始化:将文本分解为字节序列,而不是字符。这使得它可以处理不同语言的字母表或非拉丁字符集,而无需进行特殊字符编码。
2. 迭代合并:与 BPE 相同,合并最频繁的相邻字节对,而不是字符对。

例如,对于 Unicode 编码的文本,会将每个字符转换为其对应的字节序列,然后进行类似的合并操作。

特点:
- 更通用,支持多语言和多字符集的语料库。
- 避免字符集大小问题,直接在字节级别上工作。
- 对于处理大量 Unicode 字符或混合不同语言的文本有优势。

区别:

- 划分基础:
- BPE:基于字符的划分和合并。
- BBPE:基于字节的划分和合并,更适合多字符集和多语言环境。

- 语言和字符集的处理:
- BPE:需要针对特定的字符集,转换可能涉及大量的字符预处理。
- BBPE:直接在字节层级操作,避免了字符集的复杂性,能处理所有 Unicode 字符。

- 兼容性:
- BPE:需要仔细管理和理解不同语言和字符集,可能需要特殊的预处理/后处理。
- BBPE:简单且通用,适用于所有类型的文本字符,无需关心语言或字符集。

示例说明:

假设你要处理包含中文、阿拉伯文和拉丁文字符的混合文本,
- 使用 BPE,你需要确保每种语言的字符集都作为初始符号,这可能比较复杂。
- 使用 BBPE,可以直接处理字节流,无需考虑具体的字符集,一次性解决所有问题。
  • BPE:即字节对编码。其核心思想是从字母开始,不断找词频最高、且连续的两个token合并,直到达到目标词数。
  • BBPE:BBPE核心思想将BPE的从字符级别扩展到子节(Byte)级别。BPE的一个问题是如果遇到了unicode编码,基本字符集可能会很大。BBPE就是以一个字节为一种“字符”,不管实际字符集用了几个字节来表示一个字符。这样的话,基础字符集的大小就锁定在了256(2^8)。采用BBPE的好处是可以跨语言共用词表,显著压缩词表的大小。而坏处就是,对于类似中文这样的语言,一段文字的序列长度会显著增长。因此,BBPE based模型可能比BPE based模型表现的更好。然而,BBPE sequence比起BPE来说略长,这也导致了更长的训练/推理时间。BBPE其实与BPE在实现上并无大的不同,只不过基础词表使用256的字节集。
  • WordPiece:WordPiece算法可以看作是BPE的变种。不同的是,WordPiece基于概率生成新的subword而不是下一最高频字节对。WordPiece算法也是每次从词表中选出两个子词合并成新的子词。BPE选择频数最高的相邻子词合并,而WordPiece选择使得语言模型概率最大的相邻子词加入词表。
  • Unigram:它和 BPE 以及 WordPiece 从表面上看一个大的不同是,前两者都是初始化一个小词表,然后一个个增加到限定的词汇量,而 Unigram Language Model 却是先初始一个大词表,接着通过语言模型评估不断减少词表,直到限定词汇量。
  • SentencePiece:SentencePiece它是谷歌推出的子词开源工具包,它是把一个句子看作一个整体,再拆成片段,而没有保留天然的词语的概念。一般地,它把空格也当作一种特殊字符来处理,再用BPE或者Unigram算法来构造词汇表。SentencePiece除了集成了BPE、ULM子词算法之外,SentencePiece还能支持字符和词级别的分词。

BPE(Byte Pair Encoding,字节对编码) - 朴素贝叶斯 - 博客园 (cnblogs.com)

10、交叉熵最详细的解释

一篇文章讲清楚交叉熵和KL散度 - 知乎 (zhihu.com)

cross-entropy VS square error 30 min

17.(选修)To Learn More - 逻辑回归_哔哩哔哩_bilibili

【Pytorch 】笔记六:初始化与 18 种损失函数的源码解析 (qq.com)

11、优化器

收藏版|史上最全机器学习优化器Optimizer汇总 - 知乎 (zhihu.com)

梯度下降

  • 批量梯度下降法 (Batch Gradient Descent, BGD):这里的批量指整个训练集的数据,每一步更新需要使用全部样本,收敛会比较稳定,但是每一步更新都用到全部的数据,计算太慢了,这应该是理想的最优解。

  • 随机梯度下降(Stochastic Gradient Descent, SGD):每次只选一个样本更计算,速度快,包含随机性,收敛会震荡,引入噪声。关于这里噪声的引入,我的理解是只要不是BGD,都会有一定噪声?然后学习率的波动,也会导致一定的噪声?

    可以看到上面这两种是两个极端,SGD梯度波动会很大,也容易从一个局部最优跳到另一个局部最优,不保证最优解。

  • 小批量梯度下降法(Mini-batch Gradient Descent, MBGD or SGD):上述两种方式的折中,每次随机选个样本更新梯度。

梯度下降公式中,主要是学习率和梯度,针对这两者的改进,衍生出了很多方法。

Momentum:引入物理学的动量概念。参数更新时在一定程度上保留之前更新的方向,同时又利用当前batch的梯度微调最终的更新方向,简言之就是通过积累之前的动量来 (previous_sum_of_gradient) 加速当前的梯度。

主要解决SGD的两个问题:

  • 随机梯度的方法(引入的噪声)
  • Hessian矩阵病态问题(可以理解为SGD在收敛过程中和正确梯度相比来回摆动比较大的问题)

优点

前后梯度一致的时候能够加速学习;前后梯度不一致的时候能够抑制震荡,越过局部极小值(加速收敛,减小震荡)

缺点

增加了一个超参数

NAG(Nesterov accelerated gradient):对Momentum进行改进,考虑上一时刻的动量

传统的优化算法要么将学习率设置为常数要么根据训练次数调节学习率。往往忽视了学习率其他变化的可能性

先来看一下使用统一的全局学习率的缺点可能出现的问题

  • 对于某些参数,通过算法已经优化到了极小值附近,但是有的参数仍然有着很大的梯度。
  • 如果学习率太小,则梯度很大的参数会有一个很慢的收敛速度; 如果学习率太大,则已经优化得差不多的参数可能会出现不稳定的情况。 解决方案:
  • 对每个参与训练的参数设置不同的学习率,在整个学习过程中通过一些算法自动适应这些参数的学习率。 如果损失与某一指定参数的偏导的符号相同,那么学习率应该增加; 如果损失与该参数的偏导的符号不同,那么学习率应该减小。

自适应学习率算法主要有:AdaGrad算法,RMSProp算法,Adam算法以及AdaDelta算法等

Adagrad其实是对学习率进行了一个约束,对于经常更新的参数,我们已经积累了大量关于它的知识,不希望被单个样本影响太大,希望学习速率慢一些;对于偶尔更新的参数,我们了解的信息太少,希望能从每个偶然出现的样本(稀疏特征的样本)身上多学一些,即学习速率大一些。而该方法中开始使用二阶动量,才意味着“自适应学习率”优化算法时代的到来。独立地适应所有模型参数的学习率,缩放每个参数反比于其所有梯度历史平均值总和的平方根

AdaDelta:Adagrad改进,只考虑一段窗口类的历史梯度和

RMSprop:RMSprop算是Adagrad的一种发展,和Adadelta的变体,效果趋于二者之间

Adam 除了像 Adadelta 和 RMSprop 一样存储了过去梯度的平方 vt 的指数衰减平均值 ,也像 momentum 一样保持了过去梯度 mt 的指数衰减平均值:

12、RoPE

十分钟读懂旋转编码(RoPE) - 知乎 (zhihu.com)

一文看懂 LLaMA 中的旋转式位置编码(Rotary Position Embedding) - 知乎 (zhihu.com)

RoPE 旋转位置编码,详细解释(下)NLP 面试的女生彻底说明白了_rope位置编码-CSDN博客

13、prefix tuning和promt turning

LLM微调方法:Prompt Tuning And Prefix Tuning - 知乎 (zhihu.com)

https://mp.weixin.qq.com/s?__biz=MzU4NjgzNzk4MQ==&mid=2247506905&idx=1&sn=9f8f596434059242683b60042c2c6d88&chksm=fdf7a16bca80287d11117932638e9b3a8c3b812882d69ba025575654d48baf4d89280d1e75aa#rd

https://zhuanlan.zhihu.com/p/682938889

所以他们两者的区别是什么,他们额外加的可训练参数,是不是可以理解为,Prompt直接添加在输入序列
中,Prefix添加在transfommer块里面

14、Bert最大输入长度是512,如果超出了怎么办?

滑动窗口、RNN、pooling等,这里主要说pooling,类似于cv里面的思想,首先将输入分段,然后将每一段分别输入以后,进行max pooling或者average pooling,融合成一个。

15、关于大模型的涌现能力

大语言模型的涌现能力:现象与解释 - 知乎 (zhihu.com)

16、LLaMA支持中文?

Chinese-LLaMA-Alpaca是在通用中文语料上训练了基于 sentencepiece 的20K中文词表并与原版LLaMA模型的32K词表进行合并,排除重复的token后,得到的最终中文LLaMA词表大小为49953。 这一步的目的是不是就是重新训练一个新的嵌入矩阵,矩阵的大小从32000拓展到49953,可以更方便的支持中文输入

https://github.com/ymcui/Chinese-LLaMA-Alpaca

https://github.com/LlamaFamily/Llama-Chinese

17、关于attention细节

最后计算得到的attention score矩阵,以GPT自回归为例子,句子长度为T,实际上矩阵大小就是,如果是多个head,就是。我们只考虑最后两维来举例,第一行就是第一个token的q和其余所有token的k计算,第二行就是第二个token的q和其余所有token的k计算,以此类推。所以mask attention很自然的就是一个上三角为-inf的矩阵。

key_mask又是啥?其实跟mask矩阵的意义一样,后者是为了防止前面的token看到后面的token,而前者呢?则是在batch训练或者推理的时候,因为不同句子长度不同!我们只考虑哪些key参与计算。也就是padding的部分,这部分又分为left padding和right padding,在encoder-only模型中,比如bert,一般是right padding,因为我们需要第一个cls padding;在decoder-only模型中,比如gpt,一般是left padding,其实right padding也可以,但是left padding会更简单直观,right padding需要额外记录每个句子的长度。

LLM padding 细节 - 知乎 (zhihu.com)

LLM面试_padding side_哔哩哔哩_bilibili

18、prefix LM vs casual LM

prefix LM就是encoder+decoder,但是是共享权重,其次,encoder是self-attention,decoder是mask-attention,即encoder部分是互相可见的,decoder部分是只能看到左侧的。

casual LM就是decoder only,就是经典的Auto Regressive (AR-自回归)模式,比如GPT、LLaMa系列等。

19、LLM参数量

  • layernorm一般是 其中是隐藏层维度,是layer层数,每一个维度都有两个学习参数,一个scale一个offset

  • 对于decoder模块,embedding矩阵参数为

  • QKV的参数为,其中是头的数量,是每个头的维度,是隐藏层维度,拆分的时候, ;输出矩阵维度也是,所以这一部分参数为,

  • FFN层包含两个线性层,先从映射到,再从映射到,所以参数就是

20、Qanything和RAGFLOW

来自工业界的 RAG 服务,有道 QAnything 源码全流程深度解析 - 易迟的博客 | Bryan Blog (hustyichi.github.io)

深度解读RAGFlow的深度文档理解DeepDoc - JadePeng - 博客园 (cnblogs.com)

Qanything侧重于精排,用的是自家的embedding模型和reranker模型,叫做bce-embedding-base_v1bce-reranker-base_v1,我们可以用bge的embedding和reranker。

首先都是知识库通过embedding模型转成向量,存数据库,然后粗排,就是向量检索或者文本检索,或者两者的混合,向量检索就是根据得到的向量,和query的向量,计算相似度,比如余弦、欧式、曼哈顿距离等。或者直接用query的文本和数据库的文本做文字匹配,比如bm25这种。需要注意的是,粗排不保证最相关,因为底层是基于knn类似的算法,牺牲了一部分准确度,保证时间很快。精排就是将粗排的结果,再跟query做一个相似度计算。可以理解成粗排就是bi-encoder,精排就是cross-encoder。

大模型 RAG 基础:信息检索、文本向量化及 BGE-M3 embedding 实践(2024) (arthurchiao.art)

RAGFLOW侧重于文本解析。DeepDoc 是 RAGFlow 的核心组件,它利用视觉信息和解析技术,对文档进行深度理解,提取文本、表格和图像等信息。采用了很多规则,或者很多case,都是工程化的经验。ragflow支持解析文本和原文本的一一对应和高亮。以pdf为例,用pdfplumber读取文件,PyPDF2读取pdf目录,然后加上分页逻辑,ocr处理等。

21、RAG的一些业务拓展

这块主要是针对具体业务场景:RAG 分块长距离信息缺失,Late Chunking 值得试试 - 易迟的博客 | Bryan Blog (hustyichi.github.io)

Late Chunking通过延迟分片来解决信分片息缺失问题。

传统的RAG向量化:分片、每个chunk的token向量化、pooling得到每个chunk唯一表示向量

Late Chunking:先文本直接token向量化、再分片、再pooling

值得注意的是:传统的rag支持的输入token长度为512,而Late Chunking的长度是8192。因为需要用到更多的上下文信息。

22、大模型 RAG 基础:信息检索、文本向量化及 BGE-M3 embedding 实践

大模型 RAG 基础:信息检索、文本向量化及 BGE-M3 embedding 实践(2024) (arthurchiao.art)

稀疏检索:bm25,属于sparse embedding,大部分元素是0,可以类比成one hot,词表为5w的话,就是5w维,适合关键词匹配任务

稠密检索:bert,属于dense embedding,大部分不是0,比如bert的输出,一般是768维,适合语义搜索

学习型,结合上述两者,先通过bert得到dense embedding,再进行稀疏化,得到一个sparse embedding,sparse embedding因为大部分是0,检索起来快!代表:BGE-M3

  • 引入了 Token Importance Estimation;
  • 既保留了关键词搜索能力,又利用上下文信息,丰富了 embedding 的稀疏表示;
  • 能够辨别相邻或相关的 token 的重要性,即使这些 token 在文本中没有明确出现。

BGE 是一系列 embedding 模型,扩展了 BERT 的能力。BGE-M3是基于BERT的,M3代表三个multi-能力。Multi-Functionality、Multi-Linguisticity、Multi-Granularity。

BGE-M3 通过更精细的方法来捕捉每个 token 的重要性,

  1. Token importance estimation:BERT 在分类/相似性比较时仅关注第一个 token([CLS]), BGE-M3 则扩大到关注序列中的每个 token Hi

  2. 线性变换:在 encoder 的输出层上又增加一个线性层,计算每个 token 的 importance weights Wlex

  3. 激活函数

    • WlexHi 的乘积经过 Rectified Linear Unit (ReLU) 激活函数,得到每个 token 的术语权重 Wt
    • ReLU 的结果是非负的,有助于 embedding 的稀疏性。
  4. learned sparse embedding:以上输出的是一个 sparse embedding,其中每个 token 都有一个相关的 weights,表明在整个输入文本上下文中的重要性。

    源码没看,但是貌似就是利用激活函数,来得到输出的sparse embedding。

23、KV Cache PageAttention

怎么加快大模型推理?10分钟学懂VLLM内部原理,KV Cache,PageAttention_哔哩哔哩_bilibili

KV Cache就是每次当前token的时候,会保存之前所有token的k和v,但是一般在大模型推理的时候,按照可生成最长序列长度分配显存。

VLLM解决了KV Cache可能造成预分配、未分配、显存碎片等问题。具体来说,就是参考操作系统,提出了pageattention,对显存进行分页,按需分配,不提前预分配;按Block分配,减少碎片大小;虚拟内存,方便实现调用。将KV Cache的利用率从20%-40%提升到96%。

24、PreNorm和PostNorm

为什么Pre Norm的效果不如Post Norm? - 科学空间|Scientific Spaces

Pre Norm 和 Post Norm 各自的优缺点?_prenorm和postnorm区别-CSDN博客

  • Pre Norm 在训练稳定和收敛性方面有明显的优势,所以大模型时代基本都无脑使用 Pre Norm 了。但是其可能有潜在的(表示塌陷) representation collapse 问题,也就是上限可能不如 Post Norm。

  • Post Norm 则对训练不稳定,梯度容易爆炸,学习率敏感,初始化权重敏感,收敛困难。好处是有潜在效果上的优势,到底有没有呢?也不好说,因为现在大模型训练太费钱了,Post Norm 在效果上带来的提升很可能不如多扔点数据让 Pre Norm 更快的训练出来。

苏神的观点是,Pre Norm深度有水分,同样的深度,Pre Norm深度可能更小,而宽度会变宽,Post Norm则是实打实的深度。对于模型效果来说,深度更重要。所以其实Post Norm效果更好,但是现在大模型都是Pre Norm,可能是考虑到更容易训和收敛。

25、Qwen2改进的地方

LLM系列 | 26:阿里千问Qwen模型解读、本地部署 - 知乎

详解基于调整RoPE旋转角度的大模型长度外推方法 - 知乎

如何理解 RoPE 的 NTK 扩展 - 知乎

26、关于MHA、MQA、MGA

不管是什么attention计算,输入输出维度都不变,但是在内部计算的时候,涉及到维度的变化。

MHA

假设头的数量为,那么每个头的维度为假设输入是,embedding以后变成,假设是

假设隐藏层为768。那么的映射矩阵维度都是的线性层,所以都是

由于是multihead,所以需要做一个split_head

值得注意的是,在forward中

  • 我们首先需要得到每个token对应的Query、Key、Value,这里跟QKV矩阵不要弄混了!是token经过映射矩阵以后,得到对应的Query、Key、Value。
  • 批量处理的时候,是一次性处理全部的token,维度为,然后再经过split_head,维度变成,但是会再做一个维度交换,变成得到对应的query、key、value
  • 然后再计算注意力矩阵,得到的结果仍然是一个四维的,
  • 在最后一个维度算softmax,维度不变。但是值都变成0-1之间。如果有mask矩阵,那么在softmax之前,还会与mask矩阵做一个add融合操作。
  • 将上面的输出,即attention_scores与value做一个矩阵乘法,即 matmul 得到的输出为,然后恢复成三维的
  • 最后经过矩阵,得到这一次attention计算的输出,维度还是

MQA

每个头共用同一个key value,所以映射矩阵会有些许不同。的维度还是,而变成

在forward中:

  • 同样首先一次性得到Query、Key、Value,维度分别为
  • 然后进行split_head划分,得到query、key、value维度分别为
  • 计算注意力得分,得到
  • softmax最后一个维度归一化,同理有mask的话,先add mask再softmax,维度不变
  • attention_scores与value矩阵乘法,即 matmul ,得到,同理,还原成三维的
  • 经过矩阵,得到这一次attention计算的输出,维度还是

MGA

分组数量为

MGA算是前面两者的trade-off,MHA每个头单独的qkv,MQA是每个头单独的q,公用的kv

MGA则是按照query进行分组,每组公用一个kv

所以映射矩阵也有些许不同。的维度还是,但是由于分组的存在,变成。注意这里还是二维的矩阵。

在一次forward中

  • 同样首先一次性得到Query、Key、Value,维度分别为、$B\times L \times (g d/n)B\times L \times (g d/n)$
  • 然后进行split_head划分,得到query、key、value维度分别为,其中key value维度变化过程为 —> —> —> —>
  • 计算注意力得分,得到
  • softmax最后一个维度归一化,同理有mask的话,先add mask再softmax,维度不变
  • attention_scores与value矩阵乘法,即 matmul ,得到,同理,还原成三维的
  • 经过矩阵,得到这一次attention计算的输出,维度还是

三者的代码如下

MHA:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
import torch
from torch import nn

class MutiHeadAttention(torch.nn.Module):
def __init__(self, hidden_size, num_heads):
super(MutiHeadAttention, self).__init__()
self.num_heads = num_heads
self.head_dim = hidden_size // num_heads

## 初始化Q、K、V投影矩阵
self.q_linear = nn.Linear(hidden_size, hidden_size)
self.k_linear = nn.Linear(hidden_size, hidden_size)
self.v_linear = nn.Linear(hidden_size, hidden_size)

## 输出线性层
self.o_linear = nn.Linear(hidden_size, hidden_size)

def forward(self, hidden_state, attention_mask=None):
batch_size = hidden_state.size()[0]

Query = self.q_linear(hidden_state)
Key = self.k_linear(hidden_state)
Value = self.v_linear(hidden_state)

query = self.split_head(Query)
key = self.split_head(Key)
value = self.split_head(Value)

## 计算注意力分数
attention_scores = torch.matmul(query, key.transpose(
-1, -2)) / torch.sqrt(torch.tensor(self.head_dim))

if attention_mask != None:
attention_scores += attention_mask * -1e-9

## 对注意力分数进行归一化
attention_probs = torch.softmax(attention_scores, dim=-1)

output = torch.matmul(attention_probs, value)

## 对注意力输出进行拼接
output = output.transpose(-1, -2).contiguous().view(
batch_size, -1, self.head_dim * self.num_heads)

output = self.o_linear(output)

return output

def split_head(self, x):
batch_size = x.size()[0]
return x.view(batch_size, -1, self.num_heads,
self.head_dim).transpose(1, 2)


# 设置参数
batch_size = 4
hidden_size = 768
num_heads = 8
seq_length = 10
group_num = 4

# 创建模型实例
mha_model = MutiHeadAttention(hidden_size, num_heads)
# 计算参数量(仅权重矩阵)
mq_weight_sum = sum([
param.nelement() for name, param in mha_model.named_parameters()
if 'weight' in name and name != 'o_linear.weight'
])

# 打印参数名称
for name, param in mha_model.named_parameters():
if 'weight' in name:
print(f"参数名称: {name}")

print(f"MQA 参数量(仅权重矩阵): {mq_weight_sum}")

x = torch.randn(batch_size, seq_length, hidden_size)

out = mha_model(x)

MQA:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
## 多查询注意力
import torch
from torch import nn


class MutiQueryAttention(torch.nn.Module):

def __init__(self, hidden_size, num_heads):
super(MutiQueryAttention, self).__init__()
self.num_heads = num_heads
self.head_dim = hidden_size // num_heads

## 初始化Q、K、V投影矩阵
self.q_linear = nn.Linear(hidden_size, hidden_size)
self.k_linear = nn.Linear(hidden_size, self.head_dim) ###
self.v_linear = nn.Linear(hidden_size, self.head_dim) ###

## 输出线性层
self.o_linear = nn.Linear(hidden_size, hidden_size)

def forward(self, hidden_state, attention_mask=None):
batch_size = hidden_state.size()[0]

Query = self.q_linear(hidden_state)
Key = self.k_linear(hidden_state)
Value = self.v_linear(hidden_state)

query = self.split_head(Query)
key = self.split_head(Key, 1)
value = self.split_head(Value, 1)

# 计算参数量
param_sum = sum(
[param.nelement() for param in self.q_linear.parameters()])

print(f"Total parameters: {param_sum}")
print(f"Query.shape = {Query.shape}")
print(f"query.shape = {query.shape}")
print(f"key.shape = {key.shape}")
## 计算注意力分数
attention_scores = torch.matmul(query, key.transpose(
-1, -2)) / torch.sqrt(torch.tensor(self.head_dim))

if attention_mask != None:
attention_scores += attention_mask * -1e-9

## 对注意力分数进行归一化
attention_probs = torch.softmax(attention_scores, dim=-1)

output = torch.matmul(attention_probs, value)

output = output.transpose(-1, -2).contiguous().view(
batch_size, -1, self.head_dim * self.num_heads)

output = self.o_linear(output)

return output

def split_head(self, x, head_num=None):

batch_size = x.size()[0]

if head_num == None:
return x.view(batch_size, -1, self.num_heads,
self.head_dim).transpose(1, 2)
else:
return x.view(batch_size, -1, head_num,
self.head_dim).transpose(1, 2)


# 设置参数
batch_size = 4
hidden_size = 768
num_heads = 8
seq_length = 10

# 创建模型实例
mqa_model = MutiQueryAttention(hidden_size, num_heads)
# 计算参数量(仅权重矩阵)
mq_weight_sum = sum([
param.nelement() for name, param in mqa_model.named_parameters()
if 'weight' in name and name != 'o_linear.weight'
])

# 打印参数名称
for name, param in mqa_model.named_parameters():
if 'weight' in name:
print(f"参数名称: {name}")

print(f"MQA 参数量(仅权重矩阵): {mq_weight_sum}")
x = torch.randn(batch_size, seq_length, hidden_size)

out = mqa_model(x)

MGA:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
## 分组注意力查询
import torch
from torch import nn


class GroupQueryAttention(torch.nn.Module):

def __init__(self, hidden_size, num_heads, group_num):
super(GroupQueryAttention, self).__init__()
self.num_heads = num_heads
self.head_dim = hidden_size // num_heads
self.group_num = group_num

## 初始化Q、K、V投影矩阵
self.q_linear = nn.Linear(hidden_size, hidden_size)
self.k_linear = nn.Linear(hidden_size, self.group_num * self.head_dim)
self.v_linear = nn.Linear(hidden_size, self.group_num * self.head_dim)

## 输出线性层
self.o_linear = nn.Linear(hidden_size, hidden_size)

def forward(self, hidden_state, attention_mask=None):
batch_size = hidden_state.size()[0]

query = self.q_linear(hidden_state)
key = self.k_linear(hidden_state)
value = self.v_linear(hidden_state)

query = self.split_head(query)
key = self.split_head(key, self.group_num)
value = self.split_head(value, self.group_num)

## 计算注意力分数
attention_scores = torch.matmul(query, key.transpose(
-1, -2)) / torch.sqrt(torch.tensor(self.head_dim))

if attention_mask != None:
attention_scores += attention_mask * -1e-9

## 对注意力分数进行归一化
attention_probs = torch.softmax(attention_scores, dim=-1)

output = torch.matmul(attention_probs, value)

output = output.transpose(-1, -2).contiguous().view(
batch_size, -1, self.head_dim * self.num_heads)

output = self.o_linear(output)

return output

def split_head(self, x, group_num=None):

batch_size, seq_len = x.size()[:2]

if group_num == None:
return x.view(batch_size, -1, self.num_heads,
self.head_dim).transpose(1, 2)
else:
x = x.view(batch_size, -1, group_num,
self.head_dim).transpose(1, 2)
x = x[:, :, None, :, :].expand(
batch_size, group_num, self.num_heads // group_num, seq_len,
self.head_dim).reshape(batch_size,
self.num_heads // group_num * group_num,
seq_len, self.head_dim)
return x


# 设置参数
batch_size = 4
hidden_size = 768
num_heads = 8
seq_length = 10
group_num = 4

# 创建模型实例
mga_model = GroupQueryAttention(hidden_size, num_heads, group_num)
# 计算参数量(仅权重矩阵)
mq_weight_sum = sum([
param.nelement() for name, param in mga_model.named_parameters()
if 'weight' in name and name != 'o_linear.weight'
])

# 打印参数名称
for name, param in mga_model.named_parameters():
if 'weight' in name:
print(f"参数名称: {name}")

print(f"MQA 参数量(仅权重矩阵): {mq_weight_sum}")


x = torch.randn(batch_size, seq_length, hidden_size)

out = mga_model(x)

modelscope使用

默认模型会下载到~/.cache/modelscope/hub中,如果需要修改下载目录,可以手动指定环境变量:MODELSCOPE_CACHE,modelscope会将模型和数据集下载到该环境变量指定的目录中。

huggin face镜像

开头加上

1
2
import os
os.environ["HF_ENDPOINT"] = "https://hf-mirror.com"

BBPE解读

Tokenizer
GPT4的词汇表大约是GPT2的两倍,词表越大,同样的文本,编码出来的token数量越少,几乎少了一半,这意味着对于同样最大输入token长度限制的LLM来说,编码出来的token数量越少,能够处理的信息越多,能够利用的上下文越多,但是词汇表也不是越大越好,越大意味着词嵌入矩阵更大,参数更多,需要做一个trade-off

为什么词表越大,编码出来的token越少呢?举个例子,对于输入python代码来说,里面含有很多空格,gpt2的tokenizer会将每个空格分别编码,而gpt4则会将连在一起的空格单独编码。简单来说,就是词表越大,就包含更多可能的组合,对于相同意义的文本,会压缩编码长度,压缩了编码信息,更加充分利用编码空间?

基本思路:首先将训练文本,转换成字节编码的序列ids,每个范围是0-255,然后对这个序列,统计两两结合的频率,这样会得到一个字典,key是pair,value是频率。然后通过max函数,找到频率最高的pair,然后对这个序列ids进行merge,把出现pair的地方,替换新token的编号,然后返回合并后的ids,在这个过程中,有一个全局的merges字典,统计key为哪两个编号token合并(哪个pair合并),value为新token的编号。还有一个全局的vocab,key是新token的编号,value是对应pair的字节序列相加。
这样循环实际上就已经train完了,然后save一下。会保存两个文件,model和vocab。

model文件:首先会写入一些版本信息,特殊tokens等,然后根据全局变量merges字典,进行一个逐行的写入,每一行记录词表中的哪些token能合并,比如101 32会合并

vocab文件:则是首先将merges字典翻转过来,key为新token编号,value为从哪两个编号的token合并来的。然后在vocab字典中,对每一个编号的token对应的字节序列,进行render操作,主要是删掉一些控制字符。(针对utf-8不能编码的格式,比如128-255,用replace的问号代替)。然后根据每一个新token的编号,拿出他是哪个pair合并得来的,然后分别取出每个pair对应的字节编码,并记录一下,比如[e][ ] -> [e ] 256

encode:在encode的过程中,实际上就是将输入文本,首先根据utf-8得到对应的序列ids,每个元素都是0-255的,然后通过一个while循环,不断的把这个ids序列,通过get_stats函数,拿到两两pair的频率对,然后我们找到pair中,每个pair生成的新token编号最小的那个,然后将通过merges字典找到这个新token的编号,然后进行merge操作,就是序列ids的长度,直到不能再压缩为止。其实encode的过程就是一个压缩的过程,我们需要尽可能的压缩序列。

decode:decode的过程就跟encode刚好反过来,我们通过传入的ids序列编号,通过vocab字典,拼接成一个字节编码文本,然后将这个字节编码文本进行python的内置decode操作,恢复成对应的文本

1
2
3
4
5
6
7
8
9
from minbpe import BasicTokenizer

text = open("tests/taylorswift.txt", "r", encoding="utf-8").read()
tokenizer = BasicTokenizer()
tokenizer.train(text, vocab_size=512)

tokenizer.encode("hello world") # string -> tokens [104, 101, 301, 369, 119, 291, 108, 100]

tokenizer.decode([104, 101, 301, 369, 119, 291, 108, 100]) # tokens -> string 'hello world'

HuggingFace学习笔记

Tokenizers

将输入文本,进行编码,转换成模型输入的格式,也就是数组。编码的方式有很多种,编码直接影响模型的能力。

编码包括两部分

Tokenization

1
2
3
4
5
6
7
8
9
from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")

sequence = "Using a Transformer network is simple"
tokens = tokenizer.tokenize(sequence)

print(tokens)
['Using', 'a', 'transform', '##er', 'network', 'is', 'simple']

From tokens to input IDs

1
2
3
4
ids = tokenizer.convert_tokens_to_ids(tokens)

print(ids)
[7993, 170, 11303, 1200, 2443, 1110, 3014]

也可以直接encode得到ids,而解码decoding刚好与编码相反

1
2
3
decoded_string = tokenizer.decode([7993, 170, 11303, 1200, 2443, 1110, 3014])
print(decoded_string)
'Using a Transformer network is simple'

其实关于编码,我们最终都是通过bytes函数编码成字节序列。

在看Qwen或者其他模型的vocab.json的时候,你会发现,里面没有一个汉字,是因为表现出来的是字节码对应的字符形式(也不知道这样描述对不对),如果我们把这些字符一个一个的还原成\x形式的编码,三个一组,其实就是对应那个汉字的字节码。

以Qwen2.5为例 notebook代码如下

1
2
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained('qwen\Qwen2___5-7B-Instruct')
1
2
3
4
5
6
7
8
9
10
text3 = "好"
tokens = tokenizer.tokenize(text3)
ids = tokenizer.convert_tokens_to_ids(tokens)
print(tokens)
print(ids)
print(text3.encode('utf-8'))
-------------------------------------------
['好']
[52801]
b'\xe5\xa5\xbd'
1
2
3
4
5
6
7
8
text = '好'
print(len(text))
print(tokenizer.decode([56568]))
print(bytes('\xe5\xa5\xbd', encoding='utf-8')) # 这一行是我实验乱写的
-------------------------------------------
3

b'\xc3\xa5\xc2\xa5\xc2\xbd'
1
2
3
4
5
6
7
8
9
print(bytes())
print(bytes("I love python", encoding='utf-8'))
print(bytes(11))
print(bytes([1, 2, 33]),len(bytes([1, 2, 33])))
-------------------------------------------
b''
b'I love python'
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
b'\x01\x02!' 3

为什么是一个!呢?其实就是因为在ASCII或者unicode里面,33就对应感叹号!总之,bytes函数参数很多,把bytes函数弄懂就行了!