RAG入门 - Reader(2)
文章目录
- 2. Reader - LLM 💬
- 2.1. Reader model
- 大模型量化的含义和作用
- 量化的数学原理
- 具体示例(4位量化)
- NF4量化的特殊设计
- 分块量化策略
- 精度损失的实际影响
- 为什么这样可行?
- 量化的作用
- 双重量化的含义和作用
- 双重量化的具体实现
- 双重量化的作用
- 具体示例对比
- 实际应用场景
- 2.2. Prompt
2. Reader - LLM 💬
在这一部分,LLM Reader 会读取上面检索到的内容来形成最终的答案。在这一步中,可以调整的子步骤有很多:
-
检索到的文档内容被聚合到“上下文(context)”中,有很多处理方式可供选择,比如prompt compression。
-
上下文和用户的查询被聚合在一个prompt里,提交给 LLM 来生成最终的答案。
2.1. Reader model
在选择Reader Model 也就是 LLM 时需要考虑如下几个方面:
- 我们提供给Reader Model的prompt 长度受限于模型的
max_seq_length
参数,prompt中主要的内容就是Retriever输出的检索结果,前文中的代码我们选择了 5(k=5
)个最接近的内容,每个是由 512(chunk_size=512
) 个token组成,因此我们预估需要LLM至少支持的上下文长度为 4k。
为什么是4K? 如果有5个文档,每个512个token,那么仅检索到的上下文就只有2,560个token(5 × 512 = 2,560)。4K tokens说明需要考虑除检索文档之外的其他组件:
检索上下文:2,560 tokens(5 × 512)
用户查询/问题:大约50-200 tokens
系统提示/指令:可能200-500 tokens
格式化/分隔符:最小开销
响应生成缓冲区:500-1000 tokens
所以4K tokens在仅检索文档所需的最小2,560 tokens基础上提供了合理的安全边际。这考虑了完整的提示结构,并确保模型有足够的上下文窗口来进行输入处理和响应生成。
不过,如果你的检索上下文真的固定在2,560 tokens,而其他组件很少,你可能可以使用更小的上下文窗口(比如3K)。选择4K似乎是一个保守的估计,为提示变化提供了余量,确保可靠运行。
- reader model 的选择
在这个例子中,我们使用了 HuggingFaceH4/zephyr-7b-beta
,一个小巧而强大的模型。现在每周都会有许多模型发布,你可以把这个模型替换为你想要的最新最好的模型。跟踪开源 LLM 的最佳方法是查看 Open-source LLM leaderboard。
from transformers import pipeline
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfigREADER_MODEL_NAME = "HuggingFaceH4/zephyr-7b-beta"# 为了加快推理速度,我们加载这个模型的量化版本
bnb_config = BitsAndBytesConfig(load_in_4bit=True, # 4bit 量化bnb_4bit_use_double_quant=True, # 双重量化,进一步压缩量化参数# NF4:专为神经网络权重分布优化的4位数据类型# 相比传统的均匀量化(fp4),NF4能更好地处理权重的正态分布特性# 在相同压缩率下提供更好的模型性能bnb_4bit_quant_type="nf4", # 指定量化类型为NF4(NormalFloat4)bnb_4bit_compute_dtype=torch.bfloat16,
)
model = AutoModelForCausalLM.from_pretrained(READER_MODEL_NAME, quantization_config=bnb_config)
tokenizer = AutoTokenizer.from_pretrained(READER_MODEL_NAME)READER_LLM = pipeline(model=model,tokenizer=tokenizer,task="text-generation",do_sample=True,temperature=0.2,repetition_penalty=1.1,return_full_text=False,max_new_tokens=500,
)# READER_LLM("What is 4+4? Answer:")
大模型量化的含义和作用
量化是指将神经网络中的参数(权重和激活值)从高精度的数值表示(如32位浮点数)转换为低精度表示(如8位整数)的技术。这是一种模型压缩方法,可以显著减少模型的存储空间和计算需求。
量化的数学原理
量化值 = round((实际值 - zero_point) / scale) 反量化值 = 量化值 × scale + zero_point
具体示例(4位量化)
假设我们有一组权重:
[-0.8, -0.3, 0.0, 0.2, 0.7]
步骤1:确定量化参数
最小值:-0.8
最大值:0.7
4位可表示:0-15(16个值)
scale = (0.7 - (-0.8)) / 15 = 1.5 / 15 = 0.1
zero_point = 8(大约在中间)
步骤2:量化过程
实际值 → 量化计算 → 4位值 → 反量化值 -0.8 → (-0.8 - (-0.8))/0.1 = 0 → 0 → 0×0.1 + (-0.8) = -0.8 -0.3 → (-0.3 - (-0.8))/0.1 = 5 → 5 → 5×0.1 + (-0.8) = -0.30.0 → (0.0 - (-0.8))/0.1 = 8 → 8 → 8×0.1 + (-0.8) = 0.00.2 → (0.2 - (-0.8))/0.1 = 10 → 10 → 10×0.1 + (-0.8) = 0.20.7 → (0.7 - (-0.8))/0.1 = 15 → 15 → 15×0.1 + (-0.8) = 0.7
NF4量化的特殊设计
NF4(NormalFloat4)更加巧妙,它预定义了16个量化点,专门针对神经网络权重的正态分布特性:
# NF4的16个预定义量化点(示例) nf4_values = [-1.0, -0.6961928009986877, -0.5250730514526367, -0.39491748809814453,-0.28444138169288635, -0.18477343022823334, -0.09105003625154495, 0.0,0.07958029955625534, 0.16093020141124725, 0.24611230194568634, 0.33791524171829224,0.44070982933044434, 0.5626170039176941, 0.7229568362236023, 1.0 ]
每个4位值(0-15)直接映射到这些预设的浮点值,无需额外的scale计算。
分块量化策略
实际应用中,通常采用分块量化来提高精度:
# 将权重矩阵分成小块,每块独立量化 weight_tensor = torch.randn(1024, 1024) # 大权重矩阵 block_size = 64 # 每64个参数为一块 for i in range(0, 1024, block_size):block = weight_tensor[i:i+block_size]# 每个块有自己的scale和zero_pointscale_i = (block.max() - block.min()) / 15zero_point_i = block.min()# 对这个块进行量化...
精度损失的实际影响
让我用一个实际例子说明:
# 原始权重(某层的一部分) original_weights = [2.341, -1.892, 0.156, -0.023, 1.677] # 4位量化后(假设scale=0.3, zero_point=-2.0) quantized_weights = [2.4, -1.8, 0.0, 0.0, 1.8] # 精度损失很小,模型性能基本保持
为什么这样可行?
神经网络的鲁棒性:研究表明,神经网络对权重的小幅扰动具有很强的容忍性。
分布特性:大多数权重值集中在较小的范围内,4位量化能够捕获主要的信息。
整体优化:虽然单个权重有误差,但在整个网络的计算过程中,这些误差往往会相互抵消。
量化的作用
内存节省:将32位浮点数转换为8位整数,可以将模型大小减少约75%。例如,一个原本需要28GB内存的7B参数模型,量化后可能只需要7GB。
计算加速:整数运算比浮点运算更快,特别是在专用硬件上。量化模型的推理速度可以提升2-4倍。
能耗降低:低精度计算消耗更少的电力,这对移动设备和边缘计算场景尤为重要。
部署便利:量化后的模型可以在资源受限的设备上运行,扩大了应用范围。
双重量化的含义和作用
双重量化(Double Quantization)是QLoRA(Quantized Low-Rank Adaptation)技术中的一个关键创新,它对量化过程本身进行进一步的量化。
在传统量化中,我们需要存储:
量化后的权重
量化参数(如缩放因子和零点)
双重量化的思路是:既然量化参数也占用存储空间,为什么不对这些参数也进行量化?
双重量化的具体实现
第一层量化:将FP32权重量化为INT4
原始权重:32位浮点数
量化后:4位整数
需要存储:量化权重 + 缩放因子(FP32)
第二层量化:对缩放因子进行量化
缩放因子:从FP32量化为FP8
进一步减少存储需求
双重量化的作用
更高的压缩率:在QLoRA中,双重量化可以将内存使用量进一步减少约0.4GB/1B参数,相比单层量化节省约10-15%的额外空间。
保持精度:尽管进行了两层量化,通过精心设计的量化策略,模型性能下降很小。
具体示例对比
以一个1B参数的模型为例:
无量化:
存储需求:4GB(1B × 32位)
计算:FP32运算
传统4位量化:
权重存储:0.5GB(1B × 4位)
量化参数:约0.1GB(缩放因子等)
总计:0.6GB
双重量化:
权重存储:0.5GB(1B × 4位)
量化参数:约0.06GB(缩放因子也被量化)
总计:0.56GB
额外节省:约7%的存储空间
实际应用场景
微调大模型:在使用QLoRA进行大模型微调时,双重量化可以让更大的模型在有限的GPU内存中进行训练。
边缘部署:在手机或嵌入式设备上部署大模型时,每一点内存节省都很关键。
成本优化:在云端服务中,内存使用的减少直接转化为成本节省。
通过量化和双重量化技术,我们可以在保持模型性能的同时,显著降低部署和运行成本,使大模型技术更加普及和实用。
2.2. Prompt
下面是我们提供给 Reader LLM 的 RAG 提示词模板, 在模板中我们指定了上下文{context}
和用户问题{question}
这两个需要填充的变量。
prompt_in_chat_format = [{"role": "system","content": """Using the information contained in the context,
give a comprehensive answer to the question.
Respond only to the question asked, response should be concise and relevant to the question.
Provide the number of the source document when relevant.
If the answer cannot be deduced from the context, do not give an answer.""",},{"role": "user","content": """Context:
{context}
---
Now here is the question you need to answer.Question: {question}""",},
]
RAG_PROMPT_TEMPLATE = tokenizer.apply_chat_template(prompt_in_chat_format, tokenize=False, add_generation_prompt=True
)
print(RAG_PROMPT_TEMPLATE)
<|system|>
Using the information contained in the context,
give a comprehensive answer to the question.
Respond only to the question asked, response should be concise and relevant to the question.
Provide the number of the source document when relevant.
If the answer cannot be deduced from the context, do not give an answer.
<|user|>
Context:
{context}
---
Now here is the question you need to answer.Question: {question}
<|assistant|>
我们先测试一下这个模型的效果, 我们需要把上面的模板format一下再输入给模型。
retrieved_docs_text = [doc.page_content for doc in retrieved_docs] # We only need the text of the documents
context = "\nExtracted documents:\n"
context += "".join([f"Document {str(i)}:::\n" + doc for i, doc in enumerate(retrieved_docs_text)])final_prompt = RAG_PROMPT_TEMPLATE.format(question="How to create a pipeline object?", context=context)# Redact an answer
answer = READER_LLM(final_prompt)[0]["generated_text"]
print(answer)