使用 Azure 构建具有图像理解和分层文档结构分析功能的智能 RAG
检索增强生成 (RAG) 模型的出现是自然语言处理 (NLP) 领域的一个重要里程碑。这些模型将信息检索的功能与生成语言模型相结合,产生的答案不仅准确,而且语境丰富。然而,随着数字世界扩展到文本数据之外,将图像理解和分层文档结构分析纳入 RAG 系统变得越来越重要。本文探讨了这两个元素如何显著增强 RAG 模型的功能。
了解 RAG 模型
在深入探讨图像理解和文档分析的细微差别之前,让我们先简单了解一下 RAG 模型的本质。这些系统的工作原理是首先从庞大的语料库中检索相关文档,然后使用生成模型将信息合成为连贯的响应。检索组件确保模型能够访问准确且最新的信息,而生成组件则允许创建类似人类的文本。
图像理解与结构分析
挑战
传统 RAG 模型最显著的局限性之一是无法理解和解释视觉数据。在图像随处可见的文本信息世界中,这代表着模型理解能力的巨大差距。文档不仅仅是文本字符串;它们具有结构——章节、小节、段落和列表——所有这些都传达了语义重要性。传统的 RAG 模型经常忽略这种层次结构,可能会错过理解文档的全部含义。
解决方案
为了弥补这一差距,RAG 模型可以增强计算机视觉 (CV) 功能。这涉及集成图像识别和理解模块,这些模块可以分析视觉数据、提取相关信息并将其转换为 RAG 模型可以处理的文本格式。结合分层文档结构分析涉及教 RAG 模型识别和解释文档的底层结构。
执行
- 视觉特征提取:使用预先训练的神经网络识别图像中的物体、场景和活动。
- 视觉语义:开发能够理解视觉内容的上下文和语义的算法。
- 多模态数据融合:将提取的视觉信息与文本数据相结合,为 RAG 系统创建多模态环境。
- 结构识别:实施算法来识别文档中不同级别的层次,例如标题、标题和要点。
- 语义角色标记:为文档的不同部分分配语义角色,了解每个部分的目的。
- 结构感知检索:通过考虑文档的层次结构来增强检索过程,确保使用最相关的部分进行生成。
在这篇博客中,我们将研究如何使用 Azure Document Intelligence、LangChain 和 Azure OpenAI 实现这一点。
先决条件
在实现这一点之前,我们需要一些先决条件
- GPT-4-Vision-Preview 模型已部署
- GPT-4–1106-预览模型已部署
- 已部署 text-ada-embedding 模型
- 已部署 Azure 文档智能
一旦我们获得上述信息,我们就开始吧!!
让我们导入所需的库。
从dotenv导入os导入load_dotenv
load_dotenv( 'azure.env' )
从langchain导入hub
从langchain_openai导入AzureChatOpenAI
#从 langchain_community.document_loaders 导入 AzureAIDocumentIntelligenceLoader
从doc_intelligence导入AzureAIDocumentIntelligenceLoader
从langchain_openai导入AzureOpenAIEmbeddings
从langchain.schema导入StrOutputParser
从langchain.schema.runnable导入RunnablePassthrough
从langchain.text_splitter导入MarkdownHeaderTextSplitter
从langchain.vectorstores.azuresearch导入AzureSearch
从azure.ai.documentintelligence.models导入DocumentAnalysisFeature
现在,我们将在 LangChain Document Loader 上编写一些自定义函数,这些函数可以帮助我们加载 PDF 文档。我们要做的第一件事是使用 Azure Document Intelligence,它具有将图像转换为 Markdown 格式的出色功能。让我们使用它。
导入日志记录
从typing导入 Any、迭代器、列表、可选
导入os
从langchain_core.documents导入文档
从langchain_community.document_loaders.base导入BaseLoader
从langchain_community.document_loaders.base导入BaseBlobParser
从langchain_community.document_loaders.blob_loaders导入Blob
logger = logs.getLogger(__name__)
class AzureAIDocumentIntelligenceLoader ( BaseLoader ):
"""使用 Azure Document Intelligence 加载 PDF"""
def __init__ (
self,
api_endpoint: str ,
api_key: str ,
file_path:可选[ str ] = None ,
url_path:可选[ str ] = None ,
api_version:可选[ str ] = None ,
api_model: str = "prebuilt-layout" ,
mode: str = "markdown" ,
*,
analysis_features:可选[ List [ str ]] = None ,
) -> None :
"""使用 Azure Document Intelligence (以前称为 Form Recognizer)
初始化文件处理对象。 此构造函数初始化 AzureAIDocumentIntelligenceParser 对象, 用于使用 Azure Document Intelligence API 解析文件。load 方法 生成文档,其内容表示由 mode 参数确定 。 参数: ----------- api_endpoint:str 用于 DocumentIntelligenceClient 构造的 API 端点。api_key :str 用于 DocumentIntelligenceClient 构造的 API 密钥。file_path :可选[str] 需要加载的文件的路径。 必须指定 file_path 或 url_path。url_path :可选[str]
需要加载的文件的 URL。
必须指定 file_path 或 url_path。api_version
:可选 [str]
DocumentIntelligenceClient 的 API 版本。设置为 None 以使用
`azure-ai-documentintelligence` 包中的默认值。api_model
:str
唯一文档模型名称。默认值为“prebuilt-layout”。
请注意,覆盖此默认值可能会导致不支持的
行为。mode
:可选 [str]
生成的文档的内容表示类型。
使用“single”、“page”或“markdown”。默认值为“markdown”。analysis_features
:可选 [List[str]]可选分析特征列表,每个特征都应 作为符合 `azure-ai-documentintelligence` 包中枚举 `DocumentAnalysisFeature` 的 str
传递 。默认值为 None。 示例: --------- >>> obj = AzureAIDocumentIntelligenceLoader( ... file_path =“path/to/file”, ... api_endpoint =“https://endpoint.azure.com”, ... api_key =
“ APIKEY”, ... api_version =“2023-10-31-preview”, ... api_model =“prebuilt-layout”, ... mode =“markdown” ...) “”
断言(
file_path不是 None 或url_path不是None ),“必须提供file_path或url_path” self.file_path = file_path self.url_path = url_path self.parser = AzureAIDocumentIntelligenceParser(api_endpoint =api_endpoint, api_key=api_key, api_version=api_version, api_model=api_model, mode=mode, analysis_features=analysis_features, )def lazy_load( self,)-> Iterator [Document]:"""将给定路径延迟加载为页面。""" if self.file_path is not None : Yield from self.parser.parse(self.file_path) else : Yield from self.parser.parse_url(self.url_path)
现在让我们定义相同的文档解析器。
class AzureAIDocumentIntelligenceParser ( BaseBlobParser ): """使用 Azure Document Intelligence (以前称为 Forms Recognizer)
加载 PDF 。"""
def __init__ (
self,
api_endpoint: str ,
api_key: str ,
api_version: Optional [ str ] = None ,
api_model: str = "prebuilt-layout" ,
mode: str = "markdown" ,
analysis_features: Optional [ List [ str ]] = None ,
):
从azure.ai.documentintelligence导入DocumentIntelligenceClient
从azure.ai.documentintelligence.models导入DocumentAnalysisFeature
从azure.core.credentials导入AzureKeyCredential
kwargs = {}
如果api_version不是 None : kwargs[ "api_version" ] = api_version如果analysis_features不是None : _SUPPORTED_FEATURES = [ DocumentAnalysisFeature.OCR_HIGH_RESOLUTION, ] analysis_features = [ DocumentAnalysisFeature(feature) for feature in analysis_features ] if any ( [feature not in _SUPPORTED_FEATURES for feature in analysis_features] ): logger.warning( f"当前支持的功能是:" f" {[f.value for f in _SUPPORTED_FEATURES]} . " "使用其他功能可能会导致意外行为。" ) self.client = DocumentIntelligenceClient( endpoint=api_endpoint, credential=AzureKeyCredential(api_key), headers={ "x-ms-useragent" : "langchain-parser/1.0.0" }, features=analysis_features, **kwargs, )
self.api_model = api_model
self.mode = mode
assert self.mode in [ "single" , "page" , "markdown" ]
def _generate_docs_page ( self, result: Any ) -> Iterator[Document]:
for p in result.pages:
content = " " .join([line.content for line in p.lines])
d = Document(
page_content=content,
metadata={
"page" : p.page_number,
},
)
Yield d
def _generate_docs_single ( self, file_path: str , result: Any ) -> Iterator[Document]:
md_content = include_figure_in_md(file_path, result)
Yield Document(page_content=md_content, metadata={})
def lazy_parse ( self, file_path: str ) -> Iterator[Document]:
"""延迟解析 blob。"""
blob = Blob.from_path(file_path)
使用blob.as_bytes_io()作为file_obj:
poller = self.client.begin_analyze_document(
self.api_model,
file_obj,
content_type = “application / octet-stream”,
output_content_format = “markdown” 如果self.mode == “markdown” 否则 “text”,
)
result = poller.result()
如果self.mode在[ “single”,“markdown” ]中:从self._generate_docs_single(file_path,result)中
产生 elif self.mode在[ “page” ]中:从self._generate_docs_page(result)中产生其他:引发ValueError(f“无效模式:{self.mode} ”)def parse_url(self,url:str)-> Iterator [Document]:来自azure.ai.documentintelligence.models导入AnalyzeDocumentRequest poller =自我.客户。开始分析文档(
self.api_model,
AnalyzeDocumentRequest(url_source = url),
# content_type =“application / octet-stream”,
output_content_format = “markdown” 如果self.mode == “markdown” 否则 “text”,
)
result = poller.result()
如果self.mode在[ “single”,“markdown” ]中:从self._generate_docs_single(result)中
产生 elif self.mode在[ “page” ]中:从self._generate_docs_page(result)中产生其他:引发ValueError(f“无效模式:{self.mode} ”)
如果您查看此 LangChain 文档解析器,我已包含一个名为 include_figure_in_md 的方法。此方法遍历 markdown 内容并查找所有图形,然后将每个图形替换为相同的描述。
在此之前,请让我们编写一些实用方法来帮助您从文档 PDF/Image 中裁剪图像。
从PIL导入图像
导入fitz # PyMuPDF
导入mimetypes
导入base64
从mimetypes导入guess_type
# 将本地图像编码为数据 URL 的函数
def local_image_to_data_url ( image_path ):
# 根据文件扩展名猜测图像的 MIME 类型
mime_type, _ = guess_type(image_path)
如果mime_type为 None :
mime_type = 'application/octet-stream' # 如果未找到,则使用默认 MIME 类型
# 读取并编码图像文件
with open (image_path, "rb" ) as image_file:
base64_encoded_data = base64.b64encode(image_file.read()).decode( 'utf-8' )
# 构造数据 URL
return f"data: {mime_type} ;base64, {base64_encoded_data} "
def crop_image_from_image ( image_path, page_number, bounding_box ):
"""
根据边界框裁剪图像。
:param image_path: 图像文件的路径。
:param page_number: 要裁剪的图像的页码(用于 TIFF 格式)。:
param bounding_box: 边界框的坐标元组(左、上、右、下)。:
return: 裁剪后的图像。
:rtype: PIL.Image.Image
"""
with Image. open (image_path) as img:
if img. format == "TIFF" :
# 打开 TIFF 图像
img.seek(page_number)
img = img.copy()
# 边界框的格式应为(左、上、右、下)。
cropped_image = img.crop(bounding_box)
return cropped_image
def crop_image_from_pdf_page ( pdf_path, page_number, bounding_box ):
"""
从 PDF 中的给定页面裁剪一个区域并将其作为图像返回。
:param pdf_path: PDF 文件的路径。
:param page_number: 要裁剪的页码(从 0 开始)。:
param bounding_box: 边界框的 (x0, y0, x1, y1) 坐标元组。
:return: 裁剪区域的 PIL 图像。
"""
doc = fitz. open (pdf_path)
page = doc.load_page(page_number)
# 裁剪页面。rect 需要格式为 (x0, y0, x1, y1) 的坐标。
bbx = [x * 72 for x in bounding_box]
rect = fitz.Rect(bbx)
pix = page.get_pixmap(matrix=fitz.Matrix( 300 / 72 , 300 / 72 ), clip=rect)
img = Image.frombytes( "RGB" , [pix.width, pix.height], pix.samples)
doc.close()
return img
def crop_image_from_file ( file_path, page_number, bounding_box ):
"""
从文件中裁剪图像。
参数:
file_path (str):文件的路径。page_number
(int):页码(对于 PDF 和 TIFF 文件,从 0 开始索引)。bounding_box
(tuple):格式为 (x0, y0, x1, y1) 的边界框坐标。
返回:
裁剪区域的 PIL 图像。
"""
mime_type = mimetypes.guess_type(file_path)[ 0 ]
如果mime_type == “application/pdf” :
返回crop_image_from_pdf_page(file_path、page_number、bounding_box)
否则:
返回crop_image_from_image(file_path、page_number、bounding_box)
接下来我们编写一种方法,将图像传递给 GPT-4-Vision 模型并获取该图像的描述。
从openai导入AzureOpenAI
aoai_api_base = os.getenv( "AZURE_OPENAI_ENDPOINT" )
aoai_api_key= os.getenv( "AZURE_OPENAI_API_KEY" )
aoai_deployment_name = 'gpt-4-vision' # GPT-4V 的模型部署名称
aoai_api_version = '2024-02-15-preview' # 这可能会在未来更改
MAX_TOKENS = 2000
def understand_image_with_gptv ( image_path, caption ):
"""
使用 GPT-4V 模型为图像生成描述。
参数:
- api_base (str):API 的基本 URL。-
api_key (str):用于身份验证的 API 密钥。-
deploy_name (str):部署的名称。-
api_version (str):API 的版本。-
image_path (str):图像文件。
- caption(str):图像的标题。
返回:
- img_description(str):生成的图像描述。
"""
client = AzureOpenAI(
api_key=aoai_api_key,
api_version=aoai_api_version,
base_url= f" {aoai_api_base} /openai/deployments/ {aoai_deployment_name} "
)
data_url = local_image_to_data_url(image_path)
response = client.chat.completions.create(
model=aoai_deployment_name,
messages=[
{ "role" : "system" , "content" : "你是一个乐于助人的助手。" },
{ "role" : "user" , "content" : [
{
"type" : "text" ,
"text" : f"描述此图像(注意:它有图像标题:{caption}):" if caption else "描述此图像:"
},
{
“类型”:“image_url”,
“image_url”:{
“url”:data_url
}
}
] }
],
max_tokens= 2000
)
img_description = response.choices[ 0].message.content
返回img_description
现在,一旦我们设置了实用程序方法,我们就可以导入文档智能加载器并加载文档。
从langchain_community.document_loaders导入AzureAIDocumentIntelligenceLoader
loader = AzureAIDocumentIntelligenceLoader(file_path= 'sample.pdf' ,
api_key = os.getenv( "AZURE_DOCUMENT_INTELLIGENCE_KEY" ),
api_endpoint = os.getenv( "AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT" ),
api_model= "prebuilt-layout" ,
api_version= "2024-02-29-preview" ,
mode= 'markdown' ,
analysis_features = [DocumentAnalysisFeature.OCR_HIGH_RESOLUTION])
docs = loader.load()
语义分块是自然语言处理中使用的一种强大技术,它涉及将大段文本分解为较小的、主题一致的片段或语义连贯的“块”。语义分块的主要目标是捕获和保留文本中的固有含义,使每个块包含尽可能多的语义独立信息。此过程对于各种语言模型应用至关重要,例如嵌入模型和检索增强生成 (RAG),因为它有助于克服与处理长文本序列相关的限制。通过确保输入语言模型 (LLM) 的数据在主题和上下文上连贯,语义分块增强了模型解释和生成相关且准确响应的能力。
此外,它还提高了从向量数据库中检索信息的效率,能够检索与用户意图高度相关的信息,从而减少噪音并保持语义完整性。本质上,语义分块是大量文本数据和高级语言模型的有效处理能力之间的桥梁,使其成为高效、有意义的自然语言理解和生成的基石。
让我们看一下 Markdown Header Splitter,它根据标题来分割文档。
# 根据 markdown 标头将文档分成块。
headers_to_split_on = [
( "#" , "标题 1" ),
( "##" , "标题 2" ),
( "###" , "标题 3" ),
( "####" , "标题 4" ),
( "#######" , "标题 5" ),
( "######" , "标题 6" ),
( "#######" , "标题 7" ),
( "########" , "标题 8" )
]
text_splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on)
docs_string = docs[ 0 ].page_content
docs_result = text_splitter.split_text(docs_string)
print ( "分割长度: " + str ( len (docs_result)))
让我们初始化 Azure OpenAI GPT 和 Azure OpenAI Embedding 的模型。
从langchain_openai导入AzureOpenAIEmbeddings
从langchain_community.vectorstores导入FAISS
从langchain_openai导入AzureChatOpenAI
从langchain导入hub
从langchain_core.output_parsers导入StrOutputParser
从langchain_core.runnables导入RunnablePassthrough
llm = AzureChatOpenAI(api_key = os.environ[ "AZURE_OPENAI_API_KEY" ],
api_version = "2023-12-01-preview" ,
azure_endpoint = os.environ[ "AZURE_OPENAI_ENDPOINT" ],
model= "gpt-4-1106-preview" ,
streaming= True )
aoai_embeddings = AzureOpenAIEmbeddings(
api_key=os.getenv( "AZURE_OPENAI_API_KEY" ),
azure_deployment= “text-embedding-ada-002”,
openai_api_version= “2023-12-01-preview”,
azure_endpoint=os.environ[ “AZURE_OPENAI_ENDPOINT” ]
)
现在让我们创建一个索引并将嵌入存储到 FAISS 中。
# 从文档中返回检索到的文档或某些源元数据
from operator import itemgetter
prompt = hub.pull( "rlm/rag-prompt" )
from langchain.schema.runnable import RunnableMap
index = await FAISS.afrom_documents(documents=docs_result,
embedding=aoai_embeddings)
现在让我们开始创建 RAG Chain。
def format_docs(docs):
返回 “ \ n \ n” .join( docs中的doc 的doc.page_content )retriever_base = index.as_retriever(search_type = “相似性”,search_kwargs = { “k”:5 })rag_chain_from_docs =( { “context”:lambda输入:format_docs(输入[ “documents” ]),“question”:itemgetter(“question”), } | prompt | llm | StrOutputParser())rag_chain_with_source = RunnableMap( { “documents”:retriever_base,“question”:RunnablePassthrough()} )| { “documents”:lambda输入:[doc.metadata for doc in input [ “documents” ]],“answer”:rag_chain_from_docs,}
现在让我们开始行动,让我们以下面的 PDF 示例为例并从 Plot 中提出问题。
在这里,我将根据本页的情节提出一个问题。如您所见,我得到了正确的答案以及引用。
getter(“question”), } | prompt | llm | StrOutputParser())rag_chain_with_source = RunnableMap( { “documents”:retriever_base,“question”:RunnablePassthrough()} )| { “documents”:lambda输入:[doc.metadata for doc in input [ “documents” ]],“answer”:rag_chain_from_docs,}
现在让我们开始行动,让我们以下面的 PDF 示例为例并从 Plot 中提出问题。
在这里,我将根据本页的情节提出一个问题。如您所见,我得到了正确的答案以及引用。
希望你喜欢这篇博客。如果你想阅读更多类似的博客,请点赞并关注我。
博客原文:https://dz.closeai.cc/forum.php?mod=viewthread&tid=167