出处:打破文本边界:如何进行多模态RAG评测

在现代信息处理与检索系统中,如何有效地从这些包括了文本,图像,视频等多模态混合内容中,提取和利用信息是一个重要的研究方向。

一般的检索增强生成(RAG,Retrieval-Augmented Generation)方法主要依赖于文本数据,常常忽略了图像中的丰富信息。多模态大型语言模型(MLLM)的出现.为这一问题提供了新的解决方案。例如,GPT-4o和Qwen-VL等多模态大模型,不仅能够理解和生成自然语言,还能解释和描述图像内容,为RAG系统在处理多模态内容时带来了新的可能性。伴之而来的问题,是如何确保多模态RAG系统在实际应用中的有效性和可靠性。

本文多模态RAG使用EvalScope, Ragas等框架,提供一套完整的多模态RAG评测实践指南,协助开发者全面评测图文多模态RAG流程。

评测流程

本文所使用的多模态RAG流程和评测流程如下:

  1. 解析文档:使用工具(本文使用Unstructured,也可使用MinerU等其他工具)将输入的文档解析为文本、表格、图像等部分。
  2. 多模态向量存储:使用多模态嵌入模型(例如CLIP)计算图像和文本向量,并存储到向量数据库中。
  3. 检索增强生成:使用相似性搜索来检索图像和文本,并将召回结果传递给多模态LLM进行答案生成。
  4. 评测模型生成结果:使用EvalScope框架,将用户输入检索上下文模型输出标准答案(可选) 这四部分输入给法官大模型,模型将根据给定的指标评测整个RAG生成的效果。

评测指标

EvalScope支持RAG和多模态RAG的独立评测端到端评测

  • 独立评测:单独评测检索模块,评测指标包括指标包括 命中率(Hit Rate)、平均排名倒数(Mean Reciprocal Rank, MRR)、归一化折扣累积增益(Normalized Discounted Cumulative Gain, NDCG)、准确率(Precision) 等,这些指标用于测量系统在根据查询或任务排名项目方面的有效性。
  • 端到端评测:评测RAG模型对给定输入生成的输出内容,包括模型生成答案与输入查询的相关性和对齐程度。从内容生成目标视角来评测可以将评测划分为无参考答案有参考答案

    • 无参考答案评测指标包括上下文相关性(Context Relevance)、忠实度(Faithfulness) 等;
    • 而有参考答案评测指标包括正确性(Answer Correctness)、BLEUROUGE等。

注意:本文主要介绍多模态RAG的端到端生成评测,如果想要进行CLIP模型检索性能的评测,可以参考EvalScope所支持的CLIP Benchmark,提供自定义图文检索评测支持。

下面详细介绍本文评测所使用指标及其计算方法:

忠实度

衡量模型输出与检索上下文中的事实一致性。如果答案中所有的陈述都可以从提供的视觉或文本上下文中推断出来,则认为生成的答案是忠实的。答案的得分范围为 (0,1),得分越高表示忠实度越好。

示例:

用户输入: 特斯拉 Model X 怎么样?

检索上下文:

一张特斯拉 Model X 的图片

高忠实度输出: 特斯拉 Model X 是一款由特斯拉制造的电动SUV。

低忠实度输出: 猫很可爱。

解释:"猫很可爱" 无法从特斯拉 Model X 的图片中推断出来,因此忠实度得分为 0。

相关度

标衡量模型输出与检索上下文以及用户输入的相关性。答案的得分范围为 (0,1),得分越高表示相关性越好。

示例:

用户输入: 这幅画是谁画的?

检索上下文:

一幅毕加索的画

高相关性输出: 这幅画是毕加索画的。

低相关性输出: 这是一幅美丽的画。

解释:"这是一幅美丽的画" 虽然描述了画的特性,但没有回答问题,因此相关性得分为 0。

正确性

评测涉及将模型输出与标准答案进行比对,以衡量其准确性。评分范围从0到1,分数越高,表示生成的回答与标准答案的匹配度越高,正确性更好。该指标不涉及图像模态。

示例:

用户输入: 爱因斯坦什么时候出生?

标准答案:爱因斯坦于1879年在德国出生。

高正确性回答:爱因斯坦于1879年在德国出生。

低正确性回答:爱因斯坦于1879年在西班牙出生。

解释:将标准答案和回答拆分成事实子句,并使用以下概念进行计算:

  • TP(True Positive):在标准答案和生成的回答中都存在的事实或陈述。
  • FP(False Positive):存在于生成的回答中但不存在于标准答案中的事实或陈述。
  • FN(False Negative):存在于标准答案中但不存在于生成的回答中的事实或陈述。

在该例子中:

  • TP: [爱因斯坦于1879年出生]
  • FP: [爱因斯坦于西班牙出生]
  • FN: [爱因斯坦于德国出生]

计算公式:

$$ \text{F1Score} = \frac{|TP|}{(|TP| + 0.5 \times (|FP| + |FN|))} $$

因此,低正确性回答的F1 score为0.5,而高正确性回答的F1为1。

代码实现

文档解析

from unstructured.partition.pdf import partition_pdf
import os
import json

file_path = "./pdf/plants.pdf"  # PDF文件路径
image_path = "./pdf/images"  # 图片存储路径

def extract_pdf_elements(file_path, output_image_path):
    """
    从PDF文件中提取图片、表格和文本块。
    output_image_path: 文件路径,用于存储图片(.jpg)
    file_path: 文件名
    """
    os.makedirs(output_image_path, exist_ok=True)
    return partition_pdf(
        filename=file_path,
        strategy="hi_res",
        extract_images_in_pdf=True,
        infer_table_structure=False,
        chunking_strategy="by_title",
        max_characters=1000,
        new_after_n_chars=1000,
        combine_text_under_n_chars=1000,
        hi_res_model_name="yolox",
        extract_image_block_output_dir=output_image_path,
    )

构建检索器

构建一个高效的检索器是实现图文多模态RAG模型的关键步骤之一。检索器负责从大规模的数据集中,找到与用户查询最相关的文档片段或图像,从而为生成模型提供上下文信息。

import os
import numpy as np
from langchain_chroma import Chroma
from evalscope.backend.rag_eval import VisionModel

# 创建 Chroma 实例
vectorstore = Chroma(
    collection_name="mm_rag_clip",
    # 使用ModelScope上的CLIP模型计算向量
    embedding_function=VisionModel.load(model_name="AI-ModelScope/chinese-clip-vit-large-patch14-336px")  
)

# 获取所有.jpg扩展名的图片URI
image_uris = sorted(
    [
        # 获取.jpg扩展名的文件
        os.path.join(image_path, image_name)
        for image_name in os.listdir(image_path)  
        if image_name.endswith(".jpg")  
    ]
)

# 添加图片到Chroma向量存储,也可通过add_texts添加文本向量,本文不需要
vectorstore.add_images(uris=image_uris)

# 创建检索器实例
retriever = vectorstore.as_retriever(
        search_type="similarity",  # 使用相似性搜索类型
        search_kwargs={'k': 2}  # 搜索结果返回前2个最相似项(top_k = 2)
)

#检索
docs = retriever.invoke("玉米") 

构建 RAG 流程

import os
import json
import base64
import matplotlib.pyplot as plt
from io import BytesIO
from PIL import Image

from langchain_core.messages import HumanMessage
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnableLambda, RunnablePassthrough

class RAGPipeline:
    def __init__(self, retriever, model):
        self.retriever = retriever
        self.model = model
        self.question = None
        self.context = None
        self.response = None

    def split_image_text_types(self, docs):
        """
        将文档内容分为图片和文本两类。
        docs: 文档列表
        """
        images = []
        text = []
        for doc in docs:
            doc = doc.page_content  # 提取文档内容
            if self.is_base64(doc):
                images.append(doc)
            else:
                text.append(doc)
        self.context = {"images": images, "texts": text}
        return self.context

    def prompt_func(self, data_dict):
        """
        生成提示信息,将上下文文本合并为一个字符串。
        data_dict: 包含上下文信息的数据字典
        """
        # 将上下文文本合并为一个字符串
        formatted_texts = "\n".join(data_dict["context"]["texts"])
        messages = [{"type": "text", 
                     "text": (
            f"""你是一位植物图鉴科普者,专门回答有关各种植物的问题。请根据用户输入的问题和检索到的图像提供详细且准确的回答,详细描述检索到的图片细节,结合这些细节,请确保你的回答足够详细,以方便用户理解。以下是用户的问题和相关的上下文信息:
用户问题: {self.question}
检索到的上下文信息:
{formatted_texts}
"""
        )}]

        # 如果有图片,将其添加到消息中
        for image_base64 in data_dict["context"]["images"]:
            messages.append({
                "type": "image_url",
                "image_url": {"url": f"data:image/jpeg;base64,{image_base64}"}
            })

        return [HumanMessage(content=messages)]


    def is_base64(self, s):
        """
        检查字符串是否为base64编码。
        s: 输入字符串
        """
        try:
            return base64.b64encode(base64.b64decode(s)) == s.encode()
        except Exception:
            return False

    def invoke(self, question):
        """
        调用链式操作执行检索和生成回答。
        question: 用户问题
        """
        self.question = question
        chain = (
            {
                "context": self.retriever | RunnableLambda(self.split_image_text_types),
                "question": RunnablePassthrough(),
            }
            | RunnableLambda(self.prompt_func)
            | self.model
            | StrOutputParser()
        )
        
        self.response = chain.invoke(question)
        return self.response
    
    def show_result(self):
        """
        显示最终结果,包括问题、上下文和生成的回答。
        """
        print(f"Question: {self.question}")
        print(f"Context: {self.context}")
        print(f"Response: {self.response}")
        for image_base64 in self.context.get("images", []):
            self.plt_img_base64(image_base64)

    def save_case(self, file_path):
        """
        保存案例数据到指定文件路径。
        file_path: 保存路径
        """
        case_data = {
            "user_input": self.question,
            "retrieved_contexts": self.context['texts'] + self.context['images'],
            "response": self.response
        }
        
        if os.path.exists(file_path):
            # 读取现有的案例数据
            with open(file_path, 'r', encoding='utf-8') as f:
                cases = json.load(f)
        else:
            cases = []

        # 添加新的案例数据
        cases.append(case_data)

        # 写回所有案例数据
        with open(file_path, 'w', encoding='utf-8') as f:
            json.dump(cases, f, ensure_ascii=False, indent=4)

获得评测数据

from evalscope.backend.rag_eval import LLM

model = LLM.load(
            model_name="qwen-vl-plus",
            api_base="https://dashscope.aliyuncs.com/compatible-mode/v1",
            api_key=os.getenv("DASHSCOPE_API_KEY"),
        )

pipeline = RAGPipeline(retriever, model)
for prompt in ["玉米", "西兰花", "洋芋", "水稻", "汽车"]:
    response = pipeline.invoke(f"{prompt}是什么?")
    pipeline.show_result()
    pipeline.save_case("./pdf/output.json")

示例如,

[
    {
        "user_input": "玉米是什么?",
        "retrieved_contexts": [
          "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGB...", // base64 图像
          "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGB...", // base64 图像
        ],
        "response": "玉米是一种禾本科作物,在全球范围内广泛种植作为粮食、饲料或工业原料使用。\n\n在第一张图片中,我们可以看到一个正在生长中的玉米植株。它的叶子是绿色的,并带有明显的叶脉结构。茎部呈现出鲜艳的黄色,这是由于它含有丰富的类胡萝卜素色素所致。在这根茎上,我们还可以观察到一些红色的毛状物,这实际上是玉米花序的一部分,也就是将来会发育成玉米棒的地方。\n\n第二张图片则展示了两个不同阶段的玉米植株。左边是一颗已经开花并开始形成穗轴(即玉米棒)的成熟期玉米;右边则是另一棵刚刚进入抽穗生长期的小苗,可以看到其叶片较小而颜色较浅,与成熟的玉米相比显得更为稚嫩。\n\n总的来说,无论是从形态还是生命周期的角度来看,这两幅图片都为我们提供了关于玉米这种重要农作物的重要视觉参考点。"
    },
  // ...
]

每组数据包含以下三个主要字段:

  • user_input(用户输入)

    • 这个字段包含了用户提出的问题或查询。例如,“玉米是什么?”
  • retrieved_contexts(检索到的上下文)

    • 这个字段包含了系统检索到的与用户输入相关的上下文信息。在这个例子中,retrieved_contexts 是一些 base64 编码的图像数据。
  • response(模型输出)

    • 这是系统根据用户的输入以及检索到的图片上下文信息生成的回答。例如,关于玉米的回答描述了其定义以及两张图片中的内容。
  • reference(标准答案),可选字段

    • 在评测答案正确性时需要该字段,可以使用GPT-4o等模型来模拟生成。
Last modification:May 15th, 2025 at 03:08 pm