手记

用Gemini 2.0 Flash处理PDF更便宜更高效:千万文档的处理新方法

图片来源:https://meetcody.ai/blog/gemini-1-5-flash-vs-gpt-4o/,(作者编辑

试想一下: 你从将每一页 PDF 转换成图片开始,然后将这些图片送去 OCR 识别,只为将原始文本转化为可用的 HTML 或 Markdown 格式。接下来,你需要仔细检测并重建每个表格,将内容拆分以进行语义检索,最后将它们全部插入向量数据库。这已经是一个庞大的流程,通常需要集成多个 ML/OCR 模型,成本也往往很高。

但是假如有一个大型语言模型——Google的Gemini 2.0 Flash——能够简化整个流程呢?想象一下,成本大大降低,将OCR和分块处理一步完成。本文探讨的就是这种可能性。我们将展示Gemini 2.0 Flash如何一次性将PDF转换为可以直接用于Markdown的分块文本,从而摆脱复杂的多步骤流程。然后,我们将这些分块存储在KDB.AI中进行快速向量搜索,将所有这些整合成一个比你之前见过的更加优雅和经济的RAG工作流。系好安全带——这将是一场游戏规则的改变。

这里将教你如何。

  • 使用 Gemini 2.0 Flash 将 PDF 页面直接转换成分段文本。
  • 将分段文本存储在 KDB.AI 用于向量搜索。
  • 将所有这些整合到 RAG 工作流程中。

沿途,我们将展示来自 Hacker News 讨论的真实反馈,并提及 Sergey Filimonov 的博客,该博客首次以近乎完美的精度测量了每美元约 6,000 页的内容。

图片来自:https://www.sergey.fyi/articles/gemini-flash-2

重要提示: 如果你不需要原始 PDF 中的边框,这种方法比旧的 OCR 管道简单得多,也更便宜。

如果你想在 Colab 中试一试,可以看看这个笔记本:notebook

(提示:关注我的账号,即将发布第二部分,我们将实际操作,解析、导入并搜索大型数据集。敬请期待)

2. 传统的 PDF 处理问题。

为什么PDF导入这么难?

  1. 复杂布局:多栏文本、注脚、侧边栏、图片或扫描的表单。
  2. 表格提取:传统 OCR 工具常常将表格拉平为混乱的文本。
  3. 高成本:使用 GPT-4o 或其他大型语言模型可能很快就会变得昂贵,尤其是在处理数百万页时。
  4. 多种工具:你可能会使用 Tesseract 进行 OCR,使用一个布局模型来检测表格,使用一个单独的分块策略进行检索增强生成等。

许多团队最终得到一个庞大、脆弱且昂贵的工作流程。新方法是这样的:

“只需把PDF页面当作图像展示给一个多模态大型语言模型,给它一个分块提示,然后见证神奇效果。”

这时就轮到Gemini 2.0 Flash登场了。

3. 为什么选择 Gemini 2.0?

根据 Sergey Filimonov 和多位 Hacker News 的说法:https://news.ycombinator.com/item?id=43018928

  • 成本:大约每美元可以处理6,000页(通过批处理调用和最少的输出令牌)。这比许多其他解决方案(如GPT-4、专业OCR供应商等)便宜5到30倍。
  • 准确性:标准文本的准确性令人惊讶。大多数错误都是结构上的细微差异,特别是在处理表格时。

最关键的部分缺失了,那就是边界框数据。如果你需要像素级精准的覆盖图层覆盖回PDF,Gemini生成的边界框还不够准确。但如果你主要关注的是基于文本的检索或摘要,它更经济、更快、也更简单。

4. 端到端架构

以下是我们将通过代码要做的事情:

图片由作者拍摄

  1. 将PDF页转换为图像 (pdf2image)。
  2. 使用分块提示将图像发送给Gemini 2.0 Flash。
  3. 提取分块标签 <chunk>...</chunk>
  4. 使用通用嵌入模型将这些分块嵌入。
  5. 存储到KDB.AI中进行搜索。
  6. 在查询时,检索相关分块并输入LLM以生成最终答案。

接下来,我们将分代码块逐步解释每一段代码。

5. 逐步代码示例
5.1. 安装所需的依赖项并创建一个基本的表

首先,我们要安装所需的Python包。

  • google-generativeai:用于 Gemini 的 Python 客户端。
  • kdbai-client:与 KDB.AI 进行交互。
  • sentence-transformers:生成嵌入向量。
  • pdf2image:将 PDF 页面转为 PNG 图像。
  • 此外,还需安装 poppler-utils 以支持系统级的 PDF 支持。

要获取您的KDB.AI凭证,请访问KDB.AI并登录。免费的云服务提供4GB内存和30GB磁盘空间,如果量化得当,可以容纳数百万个向量数据。

    # SNIPPET 1: 安装包及设置  
    !apt-get update  
    !apt-get install -y poppler-utils  
    !pip install -q google-generativeai kdbai-client sentence-transformers pdf2image  

    import os  
    import kdbai_client as kdbai  
    from sentence_transformers import SentenceTransformer  
    # 我们连接到KDB.AI来存储我们的片段的嵌入向量  
    KDBAI_ENDPOINT = "YOUR_KDBAI_ENDPOINT"  
    KDBAI_API_KEY = "YOUR_KDBAI_API_KEY"  
    session = kdbai.Session(endpoint=KDBAI_ENDPOINT, api_key=KDBAI_API_KEY)  
    db = session.database('default')  
    print("已连接到KDB.AI:", db)

安装完之后,我们创建一个会话对象来与KDB.AI实例对话。

5.2. 创建一个矢量表

我们将定义一个简单的模式来处理分块文本和嵌入。KDB.AI 支持在‘vectors’列(向量列)上建立索引以实现相似性搜索。

    # SNIPPET 2: 定义 KDB.AI 表模式定义  
    VECTOR_DIM = 384  # 我们将使用 all-MiniLM-L6-v2 生成嵌入  

    模式定义 = [  
        {"name": "id", "type": "str"},  
        {"name": "text", "type": "str"},  
        {"name": "vectors", "type": "float32s"}  
    ]  
    # 构建一个简单的 L2 距离索引结构  
    索引 = [  
        {  
            "name": "flat_index",  
            "type": "flat",  
            "column": "vectors",  
            "参数": {"dims": VECTOR_DIM, "metric": "L2"}  
        }  
    ]  
    表名称 = "pdf_chunks"  
    try:  
        db.table(表名称).drop()  
    except kdbai.KDBAIException:  
        # 尝试删除表,如果表不存在则忽略错误  
        pass  
    表 = db.create_table(表名称, schema=模式定义, indexes=索引)  
    print(f"已创建表 '{表名称}'")

解释如下

  • 我们为每个块(chunk)存储一个 "id",文本内容 "text" 以及嵌入向量 "vectors"
  • 表使用的是平面索引,并采用 L2 距离计算。在实际生产中,如果需要更快的近似最近邻查询,可以考虑切换到 HNSW。
5.3. 将 PDF 页面转换成图片

Gemini是一个multimodal的模型,所以我们可以直接输入图像。我们使用pdf2image将每个PDF页面转换为PNG图像。

    # SNIPPET 3: 将PDF转换成图片  
    import requests  
    from pdf2image import convert_from_bytes  
    import base64  
    import io  

    pdf_url = "https://arxiv.org/pdf/2404.08865"  # 示例的PDF  
    resp = requests.get(pdf_url)  
    pdf_data = resp.content  
    pages = convert_from_bytes(pdf_data)  
    print(f"将 {len(pages)} 页 PDF 转换成了图片。完成了。")  
    # 我们将图像编码为 base64,这样方便发送给 Gemini 以便发送。  
    images_b64 = {}  
    for i, page in enumerate(pages, start=1):  
        buffer = io.BytesIO()  
        page.save(buffer, format="PNG")  
        image_data = buffer.getvalue()  
        b64_str = base64.b64encode(image_data).decode("utf-8")  
        images_b64[i] = b64_str

以下是说明:

  • convert_from_bytes 一次性处理所有页面的内容。
  • 我们将每个图像的原始 PNG 数据以 base64 字符串的形式存储,这样传递给 Gemini 的 API 就很方便。
5.4. 使用 Gemini 2.0 Flash 进行 OCR 和分段处理

让我们先初始化Gemini客户端,然后定义一个提示来指导模型按照指示行动。

  1. "将页面OCR转换为Markdown格式。"
  2. "将其拆分成每段250至1000字的长度。"
  3. "用<chunk>...</chunk>包裹每个部分。"
    # SNIPPET 4: 配置Gemini并定义分块提示内容  
    import google.generativeai as genai  

    GOOGLE_API_KEY = "YOUR_GOOGLE_API_KEY"  
    genai.configure(api_key=GOOGLE_API_KEY)  
    model = genai.GenerativeModel(model_name="gemini-2.0-flash")  
    print("Gemini模型已加载:", model)  
    CHUNKING_PROMPT = """\  
    将以下页面OCR成Markdown文本格式。表格应被格式化为HTML。  
    输出不要用三重反引号包围。  
    将文档分块,每块大约在250到1000个单词之间。  
    每个分块前后分别加上<chunk>和</chunk>标签。  
    尽可能保留所有内容,包括标题、表格等元素。  
    """

解释如下:

  • 我们加载 gemini-2.0-flash 作为模型。如果你想尝试更大/更小的版本,可以选择相应的 pro 或者 flash 版本。
  • 提示语被精心编写,以便模型输出易于解析的分隔符。
  • 我们这里处理一个 PDF 文件,但可以通过异步调用 Gemini 轻松处理数百万个 PDF 文件。
5.5. 使用单一提示处理每页。

我们将定义一个辅助函数 process_page(page_num, b64),该函数会接收并处理 base64 编码的 PNG 图片以及指令或提示信息,然后发送给 Gemini。然后我们会从响应中提取 <chunk> 块。

    # SNIPPET 5: OCR 和分块处理  
    import re  

    def process_page(page_num, image_b64):  
        # 我们将创建消息负载,如下所示:  
        payload = [  
            {  
                "inline_data": {"data": image_b64, "mime_type": "image/png"}  
            },  
            {  
                "text": CHUNKING_PROMPT  
            }  
        ]  
        try:  
            resp = model.generate_content(payload)  
            text_out = resp.text  
        except Exception as e:  
            print(f"处理第 {page_num} 页时出错:{e}")  
            return []  
        # 解析 <chunk> 块  
        chunks = re.findall(r"<chunk>(.*?)</chunk>", text_out, re.DOTALL)  
        if not chunks:  
            # 如果模型未生成分块标签,则回退  
            chunks = text_out.split("\n\n")  
        results = []  
        for idx, chunk_txt in enumerate(chunks):  
            # 存储每个分块的 ID 和文本  
            results.append({  
                "id": f"page_{page_num}_chunk_{idx}",  
                "text": chunk_txt.strip()  
            })  
        return results  
    all_chunks = []  
    for i, b64_str in images_b64.items():  
        page_chunks = process_page(i, b64_str)  
        all_chunks.extend(page_chunks)  
    print(f"提取的总分块数量:{len(all_chunks)}")

解释

  1. inline_data:告诉 Gemini 我们传递的是一个 PNG 图片。
  2. 我们还加入了文本分块的提示。
  3. 模型返回一个大的字符串。我们通过查找 <chunk>...</chunk> 来分隔每一部分。
  4. 如果没有发现 <chunk> 标签,我们将通过双换行符来分割内容。
5.6. 将片段嵌入并存储在KDB.AI中

现在我们有了分块的文本,可以用 all-MiniLM-L6-v2 模型嵌入并上传到 KDB.AI。这并不是为这个任务提供的最佳嵌入模型,但在这个例子中它就足够用了。

    # 代码片段6: 嵌入与数据插入  
    embed_model = SentenceTransformer("all-MiniLM-L6-v2")  

    chunk_texts = [ch["text"] for ch in all_chunks]  
    embeddings = embed_model.encode(chunk_texts)  
    embeddings = embeddings.astype("float32")  
    import pandas as pd  
    row_list = []  
    for idx, ch_data in enumerate(all_chunks):  
        row_list.append({  
            "id": ch_data["id"],  
            "text": ch_data["text"],  
            "特征向量": embeddings[idx].tolist()  
        })  
        # 将片段数据添加到行列表中  
    df = pd.DataFrame(row_list)  
    table.insert(df)  
    print(f"成功插入了 {len(df)} 个片段到 '{table_name}' 中。")

解释一下吧:
此处为空白,待补充内容

  • 嵌入向量数据输出为 numpy.float32 类型的数组,形状为 (num_chunks, 384) 的数组。
  • 我们将其转换为 Python 列表并放入 DataFrame 中。
  • 然后我们使用 .insert() 将其插入 KDB.AI 表中。

这时,每个块都可以在向量空间中搜索。运行一下 table.query() 就会发现它们都存储在KDB中。

6. 查询和构建 RAG 流程

我们现在可以嵌入用户的查询,获取最相关的片段,并将它们传递给_任何_大语言模型进行最终问答。

6.1. 相似搜索
    # SNIPPET 7: RAG的向量查询
    user_query = "这篇论文是如何处理多列格式文本的?"
    qvec = embed_model.encode(user_query).astype("float32")

    search_results = table.search(vectors={"flat_index": [qvec]}, n=3)
    retrieved_chunks = search_results[0]["text"].tolist()
    context_for_llm = "\n\n".join(retrieved_chunks)
    print("检索到的片段如下:\n", context_for_llm)
注意:RAG是指检索增强生成方法

解释

  • table.search() 执行向量相似性搜索。我们获取最相关的三个片段。

  • 我们将它们合并成一个字符串,进行最终的 LLM 调用请求。
6.2. 最终一代

我们将检索到的片段作为“上下文”输入到相同的Gemini模型(或任何其他LLM)中,让它生成最终答案。

    # SNIPPET 8: RAG 生成
    final_prompt = f"""使用以下背景信息回答问题:  
    背景信息:  
    {context_for_llm}  
    问题:{用户提问}  
    回答:  
    """  
    resp = model.generate_content(final_prompt)  
    print("\n=== Gemini 的最终回复 ===")  
    print(resp.text)  

作者的图片

解释

  • 这是标准的RAG方法:从顶部的几个段落中提取“上下文”,然后让LLM作出回应。
  • 如果需要特定的推理或链式思考过程,可以根据需要调整提示。
第七 注意事项与教训及来自 Hacker News 的一些教训
  1. 边界框:几位用户提到,如果你想要在原始PDF上叠加文本高亮,这可能是一个大问题。Gemini可以尝试边界框,但并不准确。
  2. 幻觉:基于LLM的OCR可以生成整个“缺失”的文本。大多数时候它非常接近实际,但有时会偏离实际,遗漏部分或生成原本不存在的新内容。有些人会进行第二次校验或提示LLM验证每行文本。
  3. 成本:如果你按单页单次调用使用,你可能会发现每美元处理的页数较少,因为单页单次调用的成本较高。通过批量调用并限制标记数量,你可以将每美元处理的页数提高到约6000页。
  4. 表格准确性:现实中的表格解析仍然可能达到80-90%的准确率。对于语义搜索或总结来说很好,但若需要精确的CSV,可能不够完美。不过,LLM在这方面只会变得越来越好,你可以轻松切换到另一个LLM,以获得更好的性能。

9. 最后的感想

  1. 用户反馈:真实团队在 HN 上用 Gemini 取代了专门的 OCR 供应商,用于 PDF 输入,节省了时间和金钱。而其他人则对边界框或绝对数值的可靠性持谨慎态度。
  2. 当边界框重要时:如果你需要精确定位 PDF 中每个片段的位置,你需要采用混合方法。(Google 可能在不久的将来解决这个问题,但目前还没有做到。)
  3. 可扩展性:处理数百万页内容?确保批处理调用并限制令牌。这样你就能达到每美元约处理 6,000 页的性价比。单页调用或大输出则会更贵。
  4. 简洁性:你可以直接跳过好几个微服务或 GPU 管道。对很多人来说,这一点本身就已经是一个很大的解脱。

简单说就是:如果你处理的是标准的PDF,并希望将其输入到向量数据库中以进行检索增强生成(RAG),那么Gemini 2.0 Flash 很可能 是达到“足够好”的文本提取的最快途径——特别是如果你不需要边界框。成本优势非常巨大,而且代码也非常简单。相比一年前,这已经取得了巨大的进步。

玩得开心!

Michael Ryaboy,KDB.AI 开发者支持者

关注我以了解大型语言模型、检索增强生成和 AI 工程的最新动态。你可以在我 LinkedInMedium 上获取更多内容。

参考文献及更多阅读
0人推荐
随时随地看视频
慕课网APP