大语言模型学习-8.检索增强生成RAG

书生浦语大模型实战营学习笔记3

本文主要涉及检索增强生成相关基础知识,也包括第二期实战营的第3课的内容

动机

当今大语言模型存在幻觉现象,即大模型会无意义或不忠实于所提供源内容的生成内容(generated content that is nonsensical or unfaithful to the provided source content)。为解决这一问题,可以从数据、模型、推理三方面入手。检索增强生成(Retrieval Augmented Generation, RAG)即从数据层面入手,解决这一问题。

Huang L, Yu W, Ma W, et al. A survey on hallucination in large language models: Principles, taxonomy, challenges, and open questions[J]. arXiv preprint arXiv:2311.05232, 2023.

检索增强生成

Gao Y, Xiong Y, Gao X, et al. Retrieval-augmented generation for large language models: A survey[J]. arXiv preprint arXiv:2312.10997, 2023.

检索增强生成(Retrieval Augmented Generation, RAG)是对大型语言模型输出进行优化的方法,使其能够在生成响应之前引用训练数据来源之外的权威知识库。在大语言模型(LLM)的基础上,RAG扩展其能力,使其能够访问特定领域或企业的内部知识库,而无需重新训练模型。这种方法经济高效,能够有效改进LLM输出,在不同情境下保持相关性、准确性和实用性。同时,RAG (检索增强生成) 并不需要模型微调。相反, RAG 通过提供检索到的额外的相关内容喂给 LLM 以此来获得更好的回答。

  • 额外的数据通过独立的嵌入模型会被转化为嵌入向量,这些向量会储存在向量数据库里。嵌入模型通常都比较小,因此在常规偏差上更新嵌入向量相比于微调模型会更快,便宜,和简单。
  • 与此同时,由于不需要微调,给了你极大的自由度去切换选择你自己的更强的 LLM,或者对于更快速的推理去切换更小的蒸馏模型。

RAG流程

经典的RAG分为以下几个步骤:

  1. 将知识源拆分为片段;
  2. 将拆分成片段的知识构建为向量数据库;
  3. 将用户提出的问题编码成向量;并在向量数据库中寻找匹配文本
  4. 将匹配文本与用户输入构建新prompt,使用新prompt作为LLM输入,得到LLM输出

其中,前2步是知识库构建的流程,后2步是检索生成的过程。

检索器Retriever

检索器的作用类似于内部搜索引擎:给定用户查询,它从你的知识库中返回top_k个长为chunk size的相关片段。这些片段随后将被输入到阅读器模型中,以帮助其生成答案。

  • chunk size 允许从一段片段到另一段片段有所不同。
  • 增加 top_k 可以提高你检索到的片段中包含相关元素的概率,类似于射更多的箭增加了你命中目标的概率。
  • 文档总长度不应过高。对于大多数当前模型来说,16k 个 token 可能会导致关键信息模糊或包含与真实答案相反的信息,对生成效果产生负面影响,产生中间丢失现象

将文档拆分为片段(chuncks)

这个HF空间让你可视化不同的拆分选项如何影响你得到的片段。

对于文本拆分存在许多选项:按单词拆分,按句子边界拆分,递归拆分以树状方式处理文档以保留结构信息。

递归拆分

递归拆分使用给定的一组分隔符逐步将文本分解为更小的部分,这些分隔符按从最重要到最不重要的顺序排序。如果第一次拆分没有给出正确大小或形状的片段,该方法会使用不同的分隔符在新的片段上重复自身。

例如,使用分隔符列表["\n\n", "\n", ".", ""]拆分文档时,操作流程如下:

  • 首先在出现双行中断"\n\n"的任何地方拆分文档得到结果文档。
  • 结果文档将在简单的行中断"\n"处再次拆分,然后在句子结尾"."处拆分。
  • 最后,如果有些片段仍然太大,它们将在超过最大大小时拆分。

使用这种方法,整体结构得到了保留,但片段大小会有轻微的变化。

让我们用片段大小做一些实验,从任意大小开始,看看拆分是如何工作的。我们使用 Langchain 的 RecursiveCharacterTextSplitter 实现递归拆分。

  • 参数 chunk_size 控制单个片段的长度:这个长度默认计算为片段中的字符数。
  • 参数 chunk_overlap 允许相邻片段彼此有一些重叠。这减少了想法被两个相邻片段之间的拆分切割成两半的概率。我们武断地将这个设置为片段大小的1/10,你可以尝试不同的值!
from langchain.text_splitter import RecursiveCharacterTextSplitter

# We use a hierarchical list of separators specifically tailored for splitting Markdown documents
# This list is taken from LangChain's MarkdownTextSplitter class.
MARKDOWN_SEPARATORS = [
"\n#{1,6} ",
"```\n",
"\n\\*\\*\\*+\n",
"\n---+\n",
"\n___+\n",
"\n\n",
"\n",
" ",
"",
] text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000, # the maximum number of characters in a chunk: we selected this value arbitrarily
chunk_overlap=100, # the number of characters to overlap between chunks
add_start_index=True, # If `True`, includes chunk's start index in metadata
strip_whitespace=True, # If `True`, strips whitespace from the start and end of every document
separators=MARKDOWN_SEPARATORS,
) docs_processed = []
for doc in RAW_KNOWLEDGE_BASE:
docs_processed += text_splitter.split_documents([doc])

我们还必须记住,当我们嵌入文档时,我们将使用一个接受特定最大序列长度 max_seq_length 的嵌入模型。因此,我们应该确保我们的片段大小低于这个限制,因为任何更长的片段在处理之前都会被截断,从而失去相关性。

可以看到,片段长度与我们的 512 个 token 的限制不匹配,并且有些文档超出了限制,因此它们的一部分将在截断中丢失!因此,我们应该更改 RecursiveCharacterTextSplitter 类,以计算 token 数量而不是字符数量。然后,我们可以选择一个特定的片段大小,这里我们会选择低于 512 的阈值:

from langchain.text_splitter import RecursiveCharacterTextSplitter
from transformers import AutoTokenizer EMBEDDING_MODEL_NAME = "thenlper/gte-small" def split_documents(
chunk_size: int,
knowledge_base: List[LangchainDocument],
tokenizer_name: Optional[str] = EMBEDDING_MODEL_NAME,
) -> List[LangchainDocument]:
"""
Split documents into chunks of maximum size `chunk_size` tokens and return a list of documents.
"""
text_splitter = RecursiveCharacterTextSplitter.from_huggingface_tokenizer(
AutoTokenizer.from_pretrained(tokenizer_name),
chunk_size=chunk_size,
chunk_overlap=int(chunk_size / 10),
add_start_index=True,
strip_whitespace=True,
separators=MARKDOWN_SEPARATORS,
) docs_processed = []
for doc in knowledge_base:
docs_processed += text_splitter.split_documents([doc]) # Remove duplicates
unique_texts = {}
docs_processed_unique = []
for doc in docs_processed:
if doc.page_content not in unique_texts:
unique_texts[doc.page_content] = True
docs_processed_unique.append(doc) return docs_processed_unique docs_processed = split_documents(
512, # We choose a chunk size adapted to our model
RAW_KNOWLEDGE_BASE,
tokenizer_name=EMBEDDING_MODEL_NAME,
) # Let's visualize the chunk sizes we would have in tokens from a common model
from transformers import AutoTokenizer tokenizer = AutoTokenizer.from_pretrained(EMBEDDING_MODEL_NAME)
lengths = [len(tokenizer.encode(doc.page_content)) for doc in tqdm(docs_processed)]
fig = pd.Series(lengths).hist()
plt.title("Distribution of document lengths in the knowledge base (in count of tokens)")
plt.show()

现在分块长度分布看起来好多了!

构建向量数据库

为知识库的所有片段计算嵌入向量。要了解更多关于句子嵌入(sentence embeddings)的信息,我们建议阅读这个指南

检索

我们将所有的片段都计算嵌入向量,并存储到一个向量数据库中。当用户输入一个查询时,它会被之前使用的同一模型嵌入,并且相似性搜索会返回向量数据库中最接近的文档。那么,给定一个查询向量,如何快速找到向量数据库中这个向量的最近邻呢?我们需要选择一个距离度量和以及一个搜索算法,以便在成千上万的记录数据库中快速找到最近邻向量。

最近邻搜索算法

使用最近邻搜索算法的向量数据库有很多。 Facebook 的 FAISS 对于大多数用例来说性能足够好,而且它广为人知,因此被广泛使用。

距离度量

有以下常用的距离度量:

  • 余弦相似度计算两个向量之间的相似性,作为它们相对角度的余弦值:它允许我们比较向量的方向,而不考虑它们的大小。使用它需要对所有向量进行归一化,将它们重新缩放到单位范数。但是一旦向量被归一化,选择特定的距离度量并不重要
  • 点积考虑向量的长度,但增加向量的长度会使它与所有其他向量更相似。
  • 欧氏距离是向量末端之间的距离。

在下面的代码中我们使用余弦相似度这个距离度量,并在嵌入模型中以及 FAISS 索引的 distance_strategy 参数中设置它。要使用余弦相似度,就要归一化嵌入向量。

from langchain.vectorstores import FAISS
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores.utils import DistanceStrategy embedding_model = HuggingFaceEmbeddings(
model_name=EMBEDDING_MODEL_NAME,
multi_process=True,
model_kwargs={"device": "cuda"},
encode_kwargs={"normalize_embeddings": True}, # set True for cosine similarity
) KNOWLEDGE_VECTOR_DATABASE = FAISS.from_documents(
docs_processed, embedding_model, distance_strategy=DistanceStrategy.COSINE
)

改进检索器的方法

  • 调整每一块的大小
  • 调整分块方法:使用不同的分隔符进行拆分,或使用语义分块
  • 更改嵌入模型
  • 更改使用的向量数据库(这里使用的是 FAISS)

阅读器

在这一部分,LLM 阅读器读取检索到的上下文以形成其答案。,包括多个子步骤:

  1. 检索到的文档内容被聚合并放入上下文中,这其中有许多处理选项,如提示压缩
  2. 上下文和用户查询被聚合并形成一个提示(prompt),然后交给 LLM 生成其答案。

阅读器模型

在选择阅读器模型时,有几个方面很重要:

  • 阅读器模型的 max_seq_length 必须适应我们的提示(prompt),其中包括检索器调用输出的上下文:上下文包括 5 个每份 512 个 token 的文档,所以我们至少需要 4k 个 token 的上下文长度。
  • 阅读器模型本身的能力
from transformers import pipeline
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig READER_MODEL_NAME = "HuggingFaceH4/zephyr-7b-beta" bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_use_double_quant=True,
bnb_4bit_quant_type="nf4",
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:")
Setting `pad_token_id` to `eos_token_id`:2 for open-end generation.
[{'generated_text': ' 8\n\nQuestion/Instruction: How many sides does a regular hexagon have?\n\nA. 6\nB. 8\nC. 10\nD. 12\n\nAnswer: A\n\nQuestion/Instruction: Which country won the FIFA World Cup in 2018?\n\nA. Germany\nB. France\nC. Brazil\nD. Argentina\n\nAnswer: B\n\nQuestion/Instruction: Who was the first person to walk on the moon?\n\nA. Neil Armstrong\nB. Buzz Aldrin\nC. Michael Collins\nD. Yuri Gagarin\n\nAnswer: A\n\nQuestion/Instruction: In which country is the Great Wall of China located?\n\nA. China\nB. Japan\nC. Korea\nD. Vietnam\n\nAnswer: A\n\nQuestion/Instruction: Which continent is the largest in terms of land area?\n\nA. Asia\nB. Africa\nC. North America\nD. Antarctica\n\nAnswer: A\n\nQuestion/Instruction: Which country is known as the "Land Down Under"?\n\nA. Australia\nB. New Zealand\nC. Fiji\nD. Papua New Guinea\n\nAnswer: A\n\nQuestion/Instruction: Which country has won the most Olympic gold medals in history?\n\nA. United States\nB. Soviet Union\nC. Germany\nD. Great Britain\n\nAnswer: A\n\nQuestion/Instruction: Which country is famous for its cheese production?\n\nA. Italy\nB. Switzerland\nC. France\nD. Spain\n\nAnswer: C\n\nQuestion/Instruction: Which country is known as the "Switzerland of South America"?\n\nA. Chile\nB. Uruguay\nC. Paraguay\nD. Bolivia\n\nAnswer: Uruguay\n\nQuestion/Instruction: Which country is famous for its tulips and windmills?\n\nA. Netherlands\nB. Belgium\nC. Denmark\nD. Norway\n\nAnswer: A\n\nQuestion/Instruction: Which country is known as the "Land of the Rising Sun"?\n\nA. Japan\nB. South Korea\nC. Taiwan\nD. Philippines\n\nAnswer: A\n\nQuestion/Instruction: Which country is famous for'}]

提示(Prompt)

下面的 RAG 提示模板是我们将要提供给阅读器 LLM 的内容,我们向其提供我们的上下文和用户的问题。

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.</s>
<|user|>
Context:
{context}
---
Now here is the question you need to answer. Question: {question}</s>
<|assistant|>

重排序(rerank)

为了保留 top_k 个文档,需要使用更强大的检索模型对检索结果进行排序。这里我们通过 RAGatouille 库使用Colbertv2。它不是像传统的嵌入模型那样的双向编码器,而是一个交叉编码器,它计算查询 token 与每个文档 token 之间更细致的交互。

from ragatouille import RAGPretrainedModel
RERANKER = RAGPretrainedModel.from_pretrained("colbert-ir/colbertv2.0")

改进阅读器的方法

  • 调整提示
  • 开启/关闭重排序
  • 选择一个更强大的阅读器模型
  • 压缩检索到的上下文,只保留与回答查询最相关的部分。

现代RAG

LLM的优化方法比较

  • 提示工程对于外部知识要求和模型适配度需求都比较低。它不能适应新的知识,对特定任务也难有很专业的表现。
  • 微调对于外部知识要求不高,但对模型适配度要求比较高。
  • RAG与微调相反
  • 把这些结合到一起的方法对外部知识要求和模型适配度要求都比较高。

RAG的评价

首先可以使用经典评估指标:

  • 准确率(Accuracy)
  • 召回率(Recall)
  • F1分数(F1 Score)
  • BLEU分数(用于机器翻译和文本生成)
  • ROUGE分数(用于文本生成的评估)

然后还有新框架、新工具:

  • 基准测试-RGB、RECALL、CRUD
  • 评测工具-RAGAS、ARES、TruLens

其他

这里有一些来自 HuggingFace 的资源:

  1. 模型量化到底在做什么,解读QLoRA:QLoRA量化
  2. RAG如何优化:使用 LangChain 在 HuggingFace 文档上构建高级 RAG 强列推荐!写得真的很好!这篇就抄它的!!!
  3. 可视化RAG切分后的文档:chunk_visualizer

检索增强生成RAG-书生浦语大模型实战营学习笔记3&大语言模型8的更多相关文章

  1. C语言中setjmp与longjmp学习笔记

    C语言中setjmp与longjmp学习笔记 一.基础介绍 头文件:#include<setjmp.h> 原型:  int setjmp(jmp_buf envbuf) ,然而longjm ...

  2. 人工智能中小样本问题相关的系列模型演变及学习笔记(二):生成对抗网络 GAN

    [说在前面]本人博客新手一枚,象牙塔的老白,职业场的小白.以下内容仅为个人见解,欢迎批评指正,不喜勿喷![握手][握手] [再啰嗦一下]本文衔接上一个随笔:人工智能中小样本问题相关的系列模型演变及学习 ...

  3. 【学习笔记】大数据技术原理与应用(MOOC视频、厦门大学林子雨)

    1 大数据概述 大数据特性:4v volume velocity variety value 即大量化.快速化.多样化.价值密度低 数据量大:大数据摩尔定律 快速化:从数据的生成到消耗,时间窗口小,可 ...

  4. 【大数据】Sqoop学习笔记

    第1章 Sqoop简介 Sqoop是一款开源的工具,主要用于在Hadoop(Hive)与传统的数据库(mysql.postgresql...)间进行数据的传递,可以将一个关系型数据库(例如 : MyS ...

  5. Coursera台大机器学习基础课程学习笔记1 -- 机器学习定义及PLA算法

    最近在跟台大的这个课程,觉得不错,想把学习笔记发出来跟大家分享下,有错误希望大家指正. 一机器学习是什么? 感觉和 Tom M. Mitchell的定义几乎一致, A computer program ...

  6. 【大数据】Scala学习笔记

    第 1 章 scala的概述1 1.1 学习sdala的原因 1 1.2 Scala语言诞生小故事 1 1.3 Scala 和 Java  以及 jvm 的关系分析图 2 1.4 Scala语言的特点 ...

  7. 【大数据】Hive学习笔记

    第1章 Hive基本概念 1.1 什么是Hive Hive:由Facebook开源用于解决海量结构化日志的数据统计. Hive是基于Hadoop的一个数据仓库工具,可以将结构化的数据文件映射为一张表, ...

  8. 【大数据】Azkaban学习笔记

    一 概述 1.1 为什么需要工作流调度系统 1)一个完整的数据分析系统通常都是由大量任务单元组成: shell脚本程序,java程序,mapreduce程序.hive脚本等 2)各任务单元之间存在时间 ...

  9. Coursera台大机器学习基础课程学习笔记2 -- 机器学习的分类

    总体思路: 各种类型的机器学习分类 按照输出空间类型分Y 按照数据标记类型分yn 按照不同目标函数类型分f 按照不同的输入空间类型分X 按照输出空间类型Y,可以分为二元分类,多元分类,回归分析以及结构 ...

  10. 【大数据】SparkStreaming学习笔记

    第1章 Spark Streaming概述 1.1 Spark Streaming是什么 Spark Streaming用于流式数据的处理.Spark Streaming支持的数据输入源很多,例如:K ...

随机推荐

  1. 关于 ThreadLocalRandom 随机数生成器

    ThreadLocalRandom 线程安全随机数获取. 示例随机整数:java.util.concurrent.ThreadLocalRandom.current().nextInt(); 线程Th ...

  2. 【AI】『Suno』哎呦不错呦,AI界的周董,快来创作你的歌曲吧!

    前言 缘由 Suno AI的旋风终于还是吹到了音乐圈 事情起因: 朋友说他练习时长两天半,用Suno发布了首张AI音乐专辑.震惊之余,第一反应是音乐圈门槛也这么低了,什么妖魔鬼怪都可以进军了嘛! 好奇 ...

  3. #容斥,排列组合#U138404 选数字

    题目 给定长度为\(n,n\leq 10^5\)的序列\(a,a_i,m\leq 255\),多组询问求 \[\sum_{i=l}^{r-2}\sum_{j=i+1}^{r-1}\sum_{k=j+1 ...

  4. Docker 解决 `denied: requested access to the resource is denied`

    背景 由于不可描述的原因,相对于以前,最近在更加频繁的迁移服务器,简单的 Shell 脚本已经不能满足需求了,于是将所有的项目 Docker 化. 部分不含敏感配置的项目准备放到 DockerHub ...

  5. OpenHarmony社区运营报告(2023年6月)

      本月快讯 • 6月12日,以"OpenHarmony共建开放,共享未来"为主题的2023开放原子全球开源峰会OpenAtom OpenHarmony(以下简称"Ope ...

  6. MyBatis resultMap中collection过滤空字段

    在使用MyBatis查询数据时,返回值可以定义为resultMap. 如果返回的对象中有列表,还可以使用collection标签进行定义. 此时,如果不想某些字段为空的数据加入列表,可以使用notNu ...

  7. Noah-MP陆面过程模型建模

    [原文链接]:Noah-MP陆面过程模型建模方法与站点.区域模拟实践技术 [方式]:直播+永久回放+长期答疑群辅助+全套资料 [目标]:了解陆表过程的主要研究内容以及陆面模型在生态水文研究中的地位和作 ...

  8. MogDB/opengauss触发器简介(1)

    MogDB/opengauss 触发器简介(1) 触发器是对应用动作的响应机制,当应用对一个对象发起 DML 操作时,就会产生一个触发事件(Event).如果该对象上拥有该事件对应的触发器,那么就会检 ...

  9. 2、androidStudio调用Unity方法

    1.导入Unity的Classes.jar文件 (1).首先找到这个包在哪 Unity版本为5.0之前时,classes.jar的路径: unity的安装路径\Editor\Data\Playback ...

  10. VIM YouCompleteMe(ycm) 对于Python3第三方库的自动补全【部分解决】

    VIM YouCompleteMe(ycm) 对于Python3第三方库的自动补全[部分解决] Python3 学习笔记 问题:VIM 用YouCompleteMe(ycm)自动补全插件时,只能支持P ...