作者:yeyan,中国地质大学 工程硕士 编辑:青稞AI
基础知识
llama2是meta最新开源的语言大模型,训练数据集2万亿token,上下文长度是由llama的2048扩展到4096,可以理解和生成更长的文本,包括7B、13B和70B三个模型,在各种基准集的测试上表现突出,最重要的是,该模型可用于研究和商业用途。
1.1 分词(tokenize)
语言模型是对文本进行推理。由于文本是字符串,但对模型来说,输入只能是数字,所以就需要将文本转成用数字来表达。最直接的想法,就是类似查字典,构造一个字典,包含文本中所有出现的词汇,比如中文,可以每个字作为词典的一个元素,构成一个列表;一个句子就可以转换成由每个词的编号(词在词典中的序号)组成的数字表达。
tokenize就是分词,一般分成3种粒度:
•word(词) 词是最简单的方式,例如英文可以按单词切分。缺点就是词汇表要包含所有词,词汇表比较大;还有比如“have”,"had"其实是有关系的,直接分词没有体现二者的关系;且容易产生oov问题(Out-Of-Vocabulary,出现没有见过的词)•char(字符) 用基础字符表示,比如英文用26个字母表示。比如 "China"拆分为"C","h","i","n","a",这样降低了内存和复杂度,但增加了任务的复杂度,一个字母没有任何语义意义,单纯使用字符可能导致模型性能的下降。•subword(子词) 结合上述2个的优缺点,遵循“尽量不分解常用词,将不常用词分解为常用的子词”的原则。例如"unbelievable"在英文中是un+形容词的组合,表否定的意思,可以分解成un”+"believable"。通过这种形式,词汇量大小不会特别大,也能学到词的关系,同时还能缓解oov问题。
subword分词主要有BPE,WorkdPiece,Unigram等方法。
BPE:字节对编码,就是是从字母开始,不断在语料库中找词频最高、且连续的token合并,直到达到目标词数。具体细节可参考【Suprit:BPE 算法原理及使用指南【深入浅出】】。
https://zhuanlan.zhihu.com/p/448147465
WordPiece88:WordPiece算法和BPE类似,区别在于WordPiece是基于概率生成新的subword而不是下一最高频字节对。
Unigram:它和 BPE 等一个不同就是,bpe是初始化一个小词表,然后一个个增加到限定的词汇量,而 Unigram 是先初始一个大词表,接着通过语言模型评估不断减少词表,直到限定词汇量。
具体可以参考【BPE、WordPiece和SentencePiece】
https://www.jianshu.com/p/d4de091d1367
和【Luke:深入理解NLP Subword算法:BPE、WordPiece、ULM】。
https://zhuanlan.zhihu.com/p/86965595
当然,现在已经有很多预训练好的词汇表,如果需要扩充新的语言,比如中文,可以先收集好语料库(训练文本),然后用SentencePiece训练自己的分词模型。具体可以看【GitHub - taishan1994/sentencepiece_chinese_bpe: 使用sentencepiece中BPE训练中文词表,并在transformers中进行使用】
https://github.com/taishan1994/sentencepiece_chinese_bpe
1.2 embedding
经过分词,文本就可以分解成用数字表示的token序列。对于一个句子,最直接的表示法就是one-hot编码。假如词汇表【我,喜,欢,吃,面】,此时词汇大小(vocab_size)大小为5,那句子“我喜欢”用one-hot编码如下图。当词汇表特别大时(llama词汇大小是3万多),句子的向量(n*vocab_size)表示也就变的比较大;另外,“喜欢”这个词出现在一起的频率其实比较高,但one-hot编码也忽略了这个特性。
![null dd954de9b52281cde3de020b9f7c91cd.png](https://i-blog.csdnimg.cn/blog_migrate/6b5d16c78ba9a311bdbbbaf88e84d6ba.png)
embedding就是将句子的向量表示压缩,具体就是词汇表的每个词映射到一个高维(d维)的特征空间。
# 一般embedding在语言模型的最开始,也就是词token操作之后
# vocab_size 词汇表大小,hidden_size 隐藏层维度大小
word_embeddings= nn.Embedding(config.vocab_size, config.hidden_size, self.padding_idx)
# input_ids是句子分词后词id,比如上述的“我喜欢”可转换成为[0,1,2],数字是在词汇表【我,喜,欢,吃,面】中的索引,即token id
embeddings = word_embeddings(input_ids) # embeddings的shape为[b,s,d],b:batch,s:seq_len,d:embedding size
embedding的每维特征都可以看出词的一个特征,比如人可以通过身高,体重,地址,年龄等多个特征表示,对于每个词embedding的每个维度的具体含义,不用人为定义,模型自己去学习。这样,在d维空间上,语义相近的词的向量就比较相似了,同时embedding还能起到降维的作用,将one-hot的[s,vocab_size]大小变成了[s,d]。
具体可参考【千寻:Embedding的作用】。
https://zhuanlan.zhihu.com/p/83814532
1.3 transformer
目前大语言模型都是基于transformer结构。
下图说明transformer的结构,第一张图代表transformer的结构,第二张图说明position embedding。
![null 6455108ee2d74a510ca5555ef8ed6143.jpeg](https://i-blog.csdnimg.cn/blog_migrate/1f8cd59be1cb65592219da01d9e7d0b5.jpeg)
![null 4bab5aaec23f9a5457ffcb78050ec42f.jpeg](https://i-blog.csdnimg.cn/blog_migrate/a3e10393e3e62526f3064f0af3a8df04.jpeg)
具体transformer可以参考【初识CV:Transformer模型详解(图解最完整版)】。
https://zhuanlan.zhihu.com/p/338817680
position embeding可以参考【Transformer Architecture: The Positional Encoding】
https://kazemnejad.com/blog/transformer_architecture_positional_encoding/
和【猛猿:Transformer学习笔记一:Positional Encoding(位置编码)】。
https://zhuanlan.zhihu.com/p/454482273
llama2模型
从transformer的结构图可见,transformer可以分成2部分,encoder和decoder,而llama只用了tranformer的decoder部分,是decoder-only结构。目前大部分生成式的语言模型都是采用这种结构,bert采用Encoder-only,google t5采用encoder-decoder结构。
至于为什么用decoder-only,参考苏剑林【为什么现在的LLM都是Decoder-only的架构?- 科学空间|Scientific Spaces】,
https://kexue.fm/archives/9529
其中提到了attention在n×d的矩阵与d×n的矩阵相乘后再加softmax(n≫d),这种形式的Attention的矩阵因为低秩问题而带来表达能力的下降,但decoder only因为用了masked attention,是一个下三角矩阵,attention一定是满秩的。也可以参考【CastellanZhang:【大模型慢学】GPT起源以及GPT系列采用Decoder-only架构的原因探讨】。
https://kexue.fm/archives/9529
llama和llama2在模型结构了基本一致,llama2结构如下图左半部分,llama共用了32个Decoder层。
其中每个decoder层如下图右半部分所示,主要是将transformer中的LayerNorm换成了RMSNorm,Multi-Head Attention换成了GQA(llama是MQA),postionnal换成了RotatyEmbedding(RoPE相对位置编码)。
![null 4100f5137f8cac6278d8c6d71ac4c6a7.jpeg](https://i-blog.csdnimg.cn/blog_migrate/d243472c6701e3646f62e6b107dce94f.jpeg)
2.2 MHA/MQA/GQA
![null a296c44279f30c057f6df5bb41811700.jpeg](https://i-blog.csdnimg.cn/blog_migrate/746f1961f5fe03428f5952776356831d.jpeg)
原始的 MHA(Multi-Head Attention),QKV 三部分有相同数量的头,且一一对应。每次做 Attention,head1 的 QKV 就做好自己运算就可以,输出时各个头加起来就行。
而 MQA 则是,让 Q 仍然保持原来的头数,但 K 和 V 只有一个头,相当于所有的 Q 头共享一组 K 和 V 头,所以叫做 Multi-Query 了。实验发现一般能提高 30%-40% 的吞吐,性能降低不太大。
GQA 综合MHA 和 MQA ,既不损失太多性能,又能利用 MQA 的推理加速。不是所有 Q 头共享一组 KV,而是分组一定头数 Q 共享一组 KV,比如上图中就是两组 Q 共享一组 KV。
具体可以参考【Andy Yang:为什么现在大家都在用 MQA 和 GQA?】
https://zhuanlan.zhihu.com/p/647130255
2.3 RoPE(相对位置编码)
起初,一般用绝对位置编码对token编码,【Taylor Wu:Transformer改进之相对位置编码(RPE)】中提到,绝对编码可能丢失了相对位置关系。
https://zhuanlan.zhihu.com/p/105001610
RoPE是利用绝对位置编码表示相对位置的一种方式,不仅保持位置编码,还能保持相对位置的关系。
将位置m转成β进制,构成一个d//2维的向量,用这个向量就能计算出位置。
![null cadd22c386347d2e2b5369a76c6bb76f.jpeg](https://i-blog.csdnimg.cn/blog_migrate/2f2004dea922d8658c1c9812657b1fe1.jpeg)
每维的数值映射到一个2*2d的矩阵,如下图所示:
![null 13908908cc3477934ea5dc4e4c92cf03.jpeg](https://i-blog.csdnimg.cn/blog_migrate/5071ea0f2cc66d1fcc666dc1a1f7a299.jpeg)
代码可以参考transformer库的modeling_llama.py中的LlamaRotaryEmbedding,rotate_half,apply_rotary_pos_emb。
详细讲解参考【Transformer升级之路:2、博采众长的旋转式位置编码 - 科学空间|Scientific Spaces】
https://spaces.ac.cn/archives/8265
和【suc16:LLM学习记录(五)--超简单的RoPE理解方式】
https://zhuanlan.zhihu.com/p/642289220
2.4 RMSNorm
layerNorm公式,其中E[x]代表均值,Var[x]代表方差。
![null 5f101218bb7582e121726307508c13c7.jpeg](https://i-blog.csdnimg.cn/blog_migrate/93fe49852c83fa7bad51b9f5f6e45770.jpeg)
RMSNorm公式
![null 2a18f1c188eb6ddd8ce9f1667cc098be.jpeg](https://i-blog.csdnimg.cn/blog_migrate/a5838b6198f893933cd0f1d572d6a848.jpeg)
对比RMSNorm和layerNorm,其实就是RMSNorm去掉了减去均值,简化了LayerNorm,减少了7%∼64%的计算时间。
推理
3.1 generate接口
llama模型推理时和一般深度学习模型不同,重新封装了一个generate方法,推理参数参考代码。
input_ids = tokenizer(['<s>Human: 介绍一下中国\n</s><s>Assistant: '], return_tensors="pt",add_special_tokens=False).input_ids.to('cuda')
generate_input = {
"input_ids":input_ids, # prompt,输入部分
"max_new_tokens":512, # 新生成的句子的token长度
"do_sample":True, # 是否采样,相当于每次不再是选择top k(beam num)个概率最大的,而是加了一些采样
"top_k":50,#在随机采样(random sampling)时,前top_k高概率的token将作为候选token被随机采样。
"top_p":0.95,#在随机采样(random sampling)时,累积概率超过top_p的token将作为候选token被随机采样,越低随机性越大,
举个例子,当top_p设定为0.6时,概率前5的token概率分别为[0.23, 0.20, 0.18, 0.11, 0.10]时,前三个token的累积概率为0.61,那么第4个token将被过滤掉,只有前三的token将作为候选token被随机采样。
"temperature":0.3, # 采样温度,较高的值如0.8会使输出更加随机,而较低的值如0.2则会使其输出更具有确定性
"num_beams":3, # 当搜索策略为束搜索(beam search)时,该参数为在束搜索(beam search)中所使用的束个数,当num_beams=1时,实际上就是贪心搜索(greedy decoding)
"repetition_penalty":1.3,#重复惩罚,
"eos_token_id":tokenizer.eos_token_id, # 结束token
"bos_token_id":tokenizer.bos_token_id, # 开始token
"pad_token_id":tokenizer.pad_token_id # pad的token
}
generate_ids = model.generate(**generate_input)
3.2 集束搜索(beam search)
在模型解码过程中,模型是根据前一个结果继续预测后边的,依次推理,此时为了生成完整的句子,需要融合多个step的输出,目标就是使得输出序列的每一步的条件概率相乘最大。
最直接的方法就是贪心算法(greedy search),每步取概率最大的输出,然后将从开始到当前步的输出作为输入,取预测下一步,直到句子结束。如下图所示,第一步先去概率最大的A,依次是B,C。这种的缺点就是如果中间某一步取值不对,将影响后续的结果。
![null eb68f55387148e4d2205f12272577a27.jpeg](https://i-blog.csdnimg.cn/blog_migrate/83d7269659da653b54dc2b608c46c7fd.jpeg)
beam search对贪心算法做了优化,在每个step取beam num个最优的tokens。下图beam num=2,第一步先选取最优的2个值{A,C};在第二步,A和C作为输入,继续预测,则有10个可能得结果,然后再取其中最大的2个,作为输入,进行第三步,取结果中概率最大的2个,得到2个结果,然后再在2个结果中取最优的一个作为输出。
![null cabed8fbbb4cbb9692e267a9e4f46805.jpeg](https://i-blog.csdnimg.cn/blog_migrate/3962930bdaba5ef26107ef25e8bba71d.jpeg)
详细可参考【https://d2l.ai/chapter_recurrent-modern/beam-search.html】 和【itachi:如何通俗的理解beam search?】
https://zhuanlan.zhihu.com/p/82829880
3.3 RoPE外推
llama1训练时token长度是2048,llama2虽然升级到了4096,但相比gpt-4的32K还是比较短,对于长文本的生成会有限制。
结合RoPE的特性,可以通过位置插值,扩展token的长度。最简单的方法就是线性插值,如下图所示:
对于下图的上半部分,对于2048之后的位置超出了训练2048的长度,模型推理时,该部分很可能就随机乱猜了,导致生成的结果不好。可以将超出的部分通过线性插值压缩到2048,这样只需要用少量4096长度的数据微调,就能达到很好的效果。
![null e98d0fdae7a1cbd13e18cb101b09bd7a.jpeg](https://i-blog.csdnimg.cn/blog_migrate/9c08697853b7fdc259c451e48fc278d1.jpeg)
另外还有NTK和动态插值算法等,transformer的llama2中实现了LlamaLinearScalingRotaryEmbedding
和LlamaDynamicNTKScalingRotaryEmbedding
。
详细可参考【Transformer升级之路:7、长度外推性与局部注意力 - 科学空间|Scientific Spaces】
https://kexue.fm/archives/9431
和【LinguaMind:浅谈LLM的长度外推】。
https://zhuanlan.zhihu.com/p/646022309
训练
训练分为预训练、指令微调、奖励模型训练。
![null 48208de5ca906bd50485d7f09cc89f0c.jpeg](https://i-blog.csdnimg.cn/blog_migrate/14ea6f97f25ba56c8121254af436f747.jpeg)
4.1 数据集
列出几个数据集。
•Awesome-Chinese-LLM中文数据集
https://github.com/HqWu-HITCS/Awesome-Chinese-LLM#3-数据集
•awesome-instruction-dataset
https://github.com/yaodongC/awesome-instruction-dataset
•awesome-instruction-datasets
https://github.com/jianzhnie/awesome-instruction-datasets
•LLaMA-Efficient-Tuning-数据集
https://github.com/hiyouga/LLaMA-Factory/blob/main/README_zh.md
4.2 预训练
(1)扩充词表
如果需要扩充词表,可以用sentencepiece训练新的词表。
具体可参考【GitHub - taishan1994/sentencepiece_chinese_bpe】
https://github.com/taishan1994/sentencepiece_chinese_bpe
然后合并新的词表,参考代码【merge_tokenizers】。
https://github.com/ymcui/Chinese-LLaMA-Alpaca/blob/main/scripts/merge_tokenizer/merge_tokenizers.py
(2)预训练
主要参考2个库。
•LLaMA-Efficient-Tuning
https://github.com/hiyouga/LLaMA-Factory/tree/main
具体参考文档,支持单GPU训练。
•Chinese-LLaMA-Alpaca-2-预训练脚本
https://github.com/ymcui/Chinese-LLaMA-Alpaca-2/wiki/pt_scripts_zh
支持多级多卡训练。
4.3 监督微调(sft)
分为参数高效微调和全部/部分参数微调。
参数高效微调一般是冻结原模型,通过adapter、lora、Prefix-tuning(前缀微调)等方法微调,peft库目前已经支持这些方法,一般用lora,结构简单。
【YBH:大模型微调(finetune)方法总结-LoRA,Adapter,Prefix-tuning,P-tuning,Prompt-tuning】
https://zhuanlan.zhihu.com/p/636481171
lora结构如下图,只需要训练右边部分A和B,通过A降维,B升维,最终权重W = W0+△W。
![null 2ce2de738a55412a306a15974e75279f.jpeg](https://i-blog.csdnimg.cn/blog_migrate/dd36374fa4a2867c2e80f154c60cba3a.jpeg)
全部/部分参数微调指直接在模型上训练,通过冻结模型的部分参数微调,主要有以下几种方式。
![null b1d87c23b66023b0dffc648fd665a492.jpeg](https://i-blog.csdnimg.cn/blog_migrate/1ccf3022255b0900ab870d83c8be22d4.jpeg)
微调代码库
•llama2官方微调教程:llama-recipes
https://github.com/facebookresearch/llama-recipes
#if running on multi-gpu machine
export CUDA_VISIBLE_DEVICES=0
python llama_finetuning.py --use_peft --peft_method lora --quantization --model_name /patht_of_model_folder/7B --output_dir Path/to/save/PEFT/model
•Llama2-Chinese-模型微调
https://github.com/FlagAlpha/Llama2-Chinese#-模型微调
•LLaMA-Efficient-Tuning-sft监督微调
https://github.com/hiyouga/LLaMA-Factory/blob/main/README_zh.md#指令监督微调
•Chinese-LLaMA-Alpaca-2-指令精调脚本
https://github.com/ymcui/Chinese-LLaMA-Alpaca-2/wiki/sft_scripts_zh
4.3 RLHF微调
llama2增加了RLHF(Reinforcement Learning from Human Feedback)基于人类反馈的强化学习。
RLHF训练流程如下图:
![null 927377748e772959a8ff260e7709f01f.jpeg](https://i-blog.csdnimg.cn/blog_migrate/15cd3c47318aa0d60b49b79e31e74565.jpeg)
(1)先对模型进行微调,具体参考4.2微调部分。
(2)训练一个奖励模型
原则上,我们可以直接用人类标注来对模型做 RLHF 微调。然而,这将需要我们给人类发送一些样本,在每轮优化后计分,这个流程需要耗费大量人工,且需要大量数据集,而人类阅读和标注的速度有限。更简单的办法就是用用人类标注集来训练一个奖励模型。奖励模型的目的是模拟人类对文本的打分。构建奖励模型有许多能用的策略: 最直接的便是预测标注 (比如根据好与坏,输出比分或者布尔值)。最佳实践是,预测结果的排序,即对每个 prompt (输入文本) 对应的两个结果yk,yj,模型预测人类标注的比分哪个更高。
(3)基于人类反馈的强化学习
有了微调的语言模型和奖励模型,可以开始执行 RL 循环了,主要分为以下三步:
•生成对 prompt (输入文本) 的反馈。•用奖励模型来对反馈评分。•对评分,进行一轮策略优化的强化学习。
参考链接
•【StackLLaMA用 RLHF 训练 LLaMA 的手把手教程】
https://github.com/huggingface/blog/blob/main/zh/stackllama.md
•【trl-stack_llama_2】
https://github.com/huggingface/trl/tree/main/examples/research_projects/stack_llama_2/scripts
•【LLaMA-Efficient-Tuning-奖励模型训练】
https://github.com/hiyouga/LLaMA-Factory/blob/main/README_zh.md#奖励模型训练
部署应用
5.1 模型部署
llama2模型部署可以参考【Llama2-Chinese#-模型部署】,支持Gradio建立web端问答平台。
https://github.com/FlagAlpha/Llama2-Chinese#-模型部署
python examples/chat_gradio.py --model_name_or_path meta-llama/Llama-2-7b-chat
5.2 模型量化
可以参考【llama.cpp】,已经支持手机上部署。
https://github.com/ggerganov/llama.cpp
5.3 与LangChain进行集成
以检索式问答任务为例,该任务使用LLM完成针对特定文档的自动问答,流程包括:文本读取、文本分割、文本/问题向量化、文本-问题匹配、将匹配文本作为上下文和问题组合生成对应Prompt中作为LLM的输入、生成回答。具体参考【Chinese-LLaMA-Alpaca-2#langchain_zh】
https://github.com/ymcui/Chinese-LLaMA-Alpaca-2/wiki/langchain_zh
5.4 Ai-agent应用
得益于llm模型的强大能力,将llm作为大脑,ai agent可以做到根据任务目标,进行思考,分解任务,然后调用相应工具完成任务。
具体参考【ai agent应用技术介绍】
https://zhuanlan.zhihu.com/p/662854606
备注:昵称-学校/公司-方向/会议(eg.ACL),进入技术/投稿群
id:DLNLPer,记得备注呦