【LLM】基于LLama2构建智能助理帮你阅读PDF文件

2024-04-27 19:47:21 浏览数 (2)

toc


前言

本文将演示如何利用 LLM 从 PDF 发票中提取数据。我将构建一个 FastAPI 服务器,该服务器将接受 PDF 文件并以 JSON 格式返回提取的数据。

我们将涵盖:

  • LangChan 用于构建 API
  • Paka,用于将 API 部署到 AWS 并水平扩展它

Paka 使用单命令方法简化了大型语言模型 (LLM) 应用程序的部署和管理。

以前,将自由格式文本转换为结构化格式通常需要我编写自定义脚本。这涉及使用 Python 或 NodeJS 等编程语言来解析文本并提取相关信息。这种方法的一个大问题是我需要为不同类型的文档编写不同的脚本。

LLM 的出现使得使用单个模型从不同的文档中提取信息成为可能。在本文中,我将向您展示如何使用 LLM 从 PDF 发票中提取信息。

我对这个项目的一些目标是:

  • 使用 HuggingFace 的开源模型 (llama2-7B),避免使用 OpenAI API 或任何其他云 AI API。
  • 构建生产就绪型 API。这意味着 API 应该能够同时处理多个请求,并且应该能够水平扩展。

一、PDF样例

我们将以 Linode 发票为例。下面是发票示例:

我们将从此发票中提取以下信息:

  • Invoice Number/ID
  • Invoice Date
  • Company Name
  • Company Address
  • Company Tax ID
  • Customer Name
  • Customer Address
  • Invoice Amount

二、构建API服务

1.PDF预处理

由于 LLM 需要文本输入,因此 PDF 文件最初必须转换为文本。对于这个任务,我们可以使用 pypdf 库或 LangChain 的 pypdf 包装器 - PyPDFLoader

代码语言:python代码运行次数:0复制
from langchain_community.document_loaders import PyPDFLoader

pdf_loader = PyPDFLoader(pdf_path)
pages = pdf_loader.load_and_split()
page_content = pages[0].page_content

print(page_content)

以下是转换结果的示例:

代码语言:shell复制
Page 1 of 1
Invoice Date: 2024-01-01T08:29:56
Remit to:
Akamai Technologies, Inc.
249 Arch St.
Philadelphia, PA 19106
USA
Tax ID(s):
United States EIN: 04-3432319Invoice To:
John Doe
1 Hacker Way
Menlo Park, CA
94025
Invoice: #25470322
Description From To Quantity Region Unit
PriceAmount TaxTotal
Nanode 1GB
debian-us-west
(51912110)2023-11-30
21:002023-12-31
20:59Fremont, CA
(us-west)0.0075 $5.00 $0.00$5.00
145 Broadway, Cambridge, MA 02142
USA
P:855-4-LINODE (855-454-6633) F:609-380-7200 W:https://www.linode.com
Subtotal (USD) $5.00
Tax Subtotal (USD) $0.00
Total (USD) $5.00
This invoice may include Linode Compute Instances that have been powered off as the data is maintained and
resources are still reserved. If you no longer need powered-down Linodes, you can remove the service
(https://www.linode.com/docs/products/platform/billing/guides/stop-billing/) from your account.
145 Broadway, Cambridge, MA 02142
USA
P:855-4-LINODE (855-454-6633) F:609-380-7200 W:https://www.linode.com

同意,该文本对人类阅读不友好。但它非常适合 LLM。

2.提取内容

我们不是使用 Python、NodeJs 或其他编程语言中的自定义脚本进行数据提取,而是通过精心制作的提示对 LLM 进行编程。一个好的提示是让 LLM 产生所需输出的关键。

对于我们的用例,我们可以编写这样的提示:

Extract all the following values: invoice number, invoice date, remit to company, remit to address, tax ID, invoice to customer, invoice to address, total amount from this invoice: <THE_INVOICE_TEXT>

根据型号的不同,此类提示可能有效,也可能无效。为了获得一个小型的、预先训练的、通用的模型,例如 llama2-7B,以产生一致的结果,我们最好使用 Few-Shot 提示技术。这是一种奇特的说法,我们应该提供我们想要的模型输出的示例。现在我们这样写模型提示:

代码语言:shell复制
Extract all the following values: invoice number, invoice date, remit to company, remit to address, tax ID, invoice to customer, invoice to address, total amount from this invoice: <THE_INVOICE_TEXT>

An example output:
{
  "invoice_number": "25470322",
  "invoice_date": "2024-01-01",
  "remit_to_company": "Akamai Technologies, Inc.",
  "remit_to_address": "249 Arch St. Philadelphia, PA 19106 USA",
  "tax_id": "United States EIN: 04-3432319",
  "invoice_to_customer": "John Doe",
  "invoice_to_address": "1 Hacker Way Menlo Park, CA 94025",
  "total_amount": "$5.00"
}

大多数 LLM 会欣赏这些示例并产生更准确和一致的结果。

但是,我们将使用 LangChain 方法处理此问题,而不是使用上述提示。虽然可以在没有LangChain的情况下完成这些任务,但它大大简化了LLM应用程序的开发。

使用 LangChain,我们用代码(Pydantic 模型)定义输出模式。

代码语言:shell复制
from langchain.output_parsers import PydanticOutputParser
from langchain_core.pydantic_v1 import BaseModel, Field


class Invoice(BaseModel):
    number: str = Field(description="invoice number, e.g. #25470322")
    date: str = Field(description="invoice date, e.g. 2024-01-01T08:29:56")
    company: str = Field(description="remit to company, e.g. Akamai Technologies, Inc.")
    company_address: str = Field(
        description="remit to address, e.g. 249 Arch St. Philadelphia, PA 19106 USA"
    )
    tax_id: str = Field(description="tax ID/EIN number, e.g. 04-3432319")
    customer: str = Field(description="invoice to customer, e.g. John Doe")
    customer_address: str = Field(
        description="invoice to address, e.g. 123 Main St. Springfield, IL 62701 USA"
    )
    amount: str = Field(description="total amount from this invoice, e.g. $5.00")


invoice_parser = PydanticOutputParser(pydantic_object=Invoice)

写下带有详细信息的字段描述。稍后,描述将用于生成提示。

然后我们需要定义提示模板,稍后将提供给 LLM。

代码语言:shell复制
from langchain_core.prompts import PromptTemplate

template = """
Extract all the following values : invoice number, invoice date, remit to company, remit to address,
tax ID, invoice to customer, invoice to address, total amount from this invoice: {invoice_text}

{format_instructions}

Only returns the extracted JSON object, don't say anything else.
"""

prompt = PromptTemplate(
    template=template,
    input_variables=["invoice_text"],
    partial_variables={
        "format_instructions": invoice_parser.get_format_instructions()
    },
)

呵呵,这不像 Few-Shot 提示那么直观。但是 invoice_parser.get_format_instructions() 将生成一个更详细的示例供 LLM 使用。

使用 LangChain 构建的已完成提示如下所示:

代码语言:shell复制
Extract all the following values : 
...
...
...
The output should be formatted as a JSON instance that conforms to the JSON schema below.

As an example, for the schema {"properties": {"foo": {"title": "Foo", "description": "a list of strings", "type": "array", "items": {"type": "string"}}}, "required": ["foo"]}
the object {"foo": ["bar", "baz"]} is a well-formatted instance of the schema. The object {"properties": {"foo": ["bar", "baz"]}} is not well-formatted.

Here is the output schema:

{"properties": {"number": {"title": "Number", "description": "invoice number, e.g. #25470322", "type": "string"}, "date": {"title": "Date", "description": "invoice date, e.g. 2024-01-01T08:29:56", "type": "string"}, "company": {"title": "Company
", "description": "remit to company, e.g. Akamai Technologies, Inc.", "type": "string"}, "company_address": {"title": "Company Address", "description": "remit to address, e.g. 249 Arch St. Philadelphia, PA 19106 USA", "type": "string"}, "tax_id"
: {"title": "Tax Id", "description": "tax ID/EIN number, e.g. 04-3432319", "type": "string"}, "customer": {"title": "Customer", "description": "invoice to customer, e.g. John Doe", "type": "string"}, "customer_address": {"title": "Customer Addre
ss", "description": "invoice to address, e.g. 123 Main St. Springfield, IL 62701 USA", "type": "string"}, "amount": {"title": "Amount", "description": "total amount from this invoice, e.g. $5.00", "type": "string"}}, "required": ["number", "date
", "company", "company_address", "tax_id", "customer", "customer_address", "amount"]}


Only returns the extracted JSON object, don't say anything else.

您可以看到提示更加详细和信息丰富。“Only returned the extracted JSON object, don't say anything else.” 是我添加的,以确保 LLM 不会输出任何其他内容。

现在,我们准备使用 LLM 进行信息提取。

代码语言:shell复制
llm = LlamaCpp(
    model_url=LLM_URL,
    temperature=0,
    streaming=False,
)

chain = prompt | llm | invoice_parser

result = chain.invoke({"invoice_text": page_content})

LlamaCpp 是 Llama2-7B 模型的客户端代理,该模型将由 Paka 托管在 AWS 中。LlamaCpp 在这里定义。当 Paka 部署 Llama2-7B 模型时,它使用很棒的 llama.cpp 项目和 llama-cpp-python 作为模型运行时。

该链是一个管道,包含提示符、LLM 和输出解析器。在此管道中,提示符被馈送到 LLM 中,输出分析器分析输出。除了在提示符中创建一次性示例外,invoice_parser还可以验证输出并返回 Pydantic 对象。

3.构建API服务

有了核心逻辑,我们的下一步是构建一个 API 端点,该端点接收 PDF 文件并以 JSON 格式提供结果。我们将使用 FastAPI 来完成此任务。

代码语言:shell复制
from fastapi import FastAPI, File, UploadFile
from uuid import uuid4

@app.post("/extract_invoice")
async def upload_file(file: UploadFile = File(...)) -> Any:
    unique_filename = str(uuid4())
    tmp_file_path = f"/tmp/{unique_filename}"

    try:
        with open(tmp_file_path, "wb") as buffer:
            shutil.copyfileobj(file.file, buffer)

        return extract(tmp_file_path) # extract is the function that contains the LLM logic
    finally:
        if os.path.exists(tmp_file_path):
            os.remove(tmp_file_path)

代码非常简单。它接受一个文件,将其保存到临时位置,然后调用提取函数来提取发票数据。

4.部署API服务

我们只走了一半。正如所承诺的那样,我们的目标是开发一个生产就绪的 API,而不仅仅是在我的本地机器上运行的原型。这涉及将 API 和模型部署到云中,并确保它们可以水平扩展。此外,我们需要收集日志和指标以进行监控和分析。这是一项艰巨的工作,而且不如构建核心逻辑有趣。幸运的是,我们有 Paka 帮助我们完成这项任务。

但在深入研究部署之前,让我们试着回答这个问题:“为什么我们需要部署模型,而不仅仅是使用 OpenAI 或 Google 的 API?要部署模型的主要原因:

  • Cost: 使用 OpenAI API 可能会因为大量数据而变得昂贵。
  • Vendor lock-in: 您可能希望避免被束缚在特定的提供商身上。
  • Flexibility: 您可能更愿意根据自己的需求定制模型,或者从 HuggingFace 中心选择开源选项。
  • Control: 您可以完全控制系统的稳定性和可扩展性。
  • Privacy: 您可能不希望将敏感数据暴露给外部各方。

现在,让我们使用 Paka 将 API 部署到 AWS:

1)基础环境
代码语言:shell复制
pip install paka

# Ensure AWS credentials and CLI are set up. 
aws configure

# Install pack CLI and verify it is working (https://buildpacks.io/docs/for-platform-operators/how-to/integrate-ci/pack/)
pack --version

# Install pulumi CLI and verify it is working (https://www.pulumi.com/docs/install/)
pulumi version

# Ensure the Docker daemon is running
docker info
2)创建配置文件

使用 CPU 实例运行模型。我们可以创建一个包含以下内容的 cluster.yaml 文件:

代码语言:shell复制
aws:
  cluster:
    name: invoice-extraction
    region: us-west-2
    namespace: default
    nodeType: t2.medium
    minNodes: 2
    maxNodes: 4
  prometheus:
    enabled: false
  tracing:
    enabled: false
  modelGroups:
    - nodeType: c7a.xlarge
      minInstances: 1
      maxInstances: 3
      name: llama2-7b
      resourceRequest:
        cpu: 3600m
        memory: 6Gi
      autoScaleTriggers:
        - type: cpu
          metadata:
            type: Utilization
            value: "50"

大多数字段都是不言自明的。modelGroups 字段是我们定义模型组的地方。在本例中,我们定义了一个名为 llama2-7b 的模型组,其实例类型为 c7a.xlarge。autoScaleTriggers 字段是我们定义自动缩放触发器的位置。我们正在定义一个 CPU 触发器,该触发器将根据 CPU 利用率扩展实例。请注意,Paka 不支持将模型组扩展到零实例,因为冷启动时间太长。我们需要保持至少一个实例处于运行状态。

要使用 GPU 实例运行模型,下面是一个集群配置示例。

3)创建集群

现在,您可以使用以下命令预配集群:

代码语言:shell复制
# Provision the cluster and update ~/.kube/config
paka cluster up -f cluster.yaml -u

上述命令将创建具有指定配置的新 EKS 集群。它还将使用新的集群信息更新 ~/.kube/config 文件。Paka 从 HuggingFace 中心下载 llama2-7b 模型并将其部署到集群。

4)部署服务

现在,我们想将 FastAPI 应用部署到集群。我们可以通过运行以下命令来执行此操作:

代码语言:shell复制
# Change the directory to the source code directory
paka function deploy --name invoice-extraction --source . --entrypoint serve

FastAPI 应用部署为函数。这意味着它是无服务器的。只有当有请求时,才会调用该函数。

在后台,该命令将使用构建包构建 Docker 映像,然后将其推送到 Elastic Container Registry。然后,映像将作为函数部署到集群中。

5)测试API

首先,我们需要获取 FastAPI 应用的 URL。我们可以通过运行以下命令来执行此操作:

代码语言:shell复制
paka function list

如果所有步骤都成功,则该函数应显示在标记为“READY”的列表中。默认情况下,可通过公共 REST API 终结点访问该函数,其格式通常类似于 http://invoice-extraction.default.50.112.90.64.sslip.io。

您可以通过使用 curl 或其他 HTTP 客户端向端点发送 POST 请求来测试 API。下面是一个使用 curl 的示例:

代码语言:shell复制
curl -X POST -H "Content-Type: multipart/form-data" -F "file=@/path/to/invoices/invoice-2024-02-29.pdf" http://invoice-extraction.default.xxxx.sslip.io/extract_invoice

如果发票提取成功,响应将显示结构化数据,如下所示:

代码语言:shell复制
{"number":"#25927345","date":"2024-01-31T05:07:53","company":"Akamai Technologies, Inc.","company_address":"249 Arch St. Philadelphia, PA 19106 USA","tax_id":"United States EIN: 04-3432319","customer":"John Doe","customer_address":"1 Hacker Way Menlo Park, CA  94025","amount":"$5.00"}
6)监控

出于监控目的,Paka 会自动将所有日志发送到 CloudWatch,以便直接在 CloudWatch 控制台中查看这些日志。此外,您可以在 cluster.yaml 中启用 Prometheus 来收集预定义的指标。

小节

本文演示了如何使用 LLM 从 PDF 发票中提取数据。我们构建了一个FastAPI服务器,能够接收PDF文件并以JSON格式返回信息。随后,我们使用 Paka 在 AWS 上部署了 API,并启用了水平扩展。

我正在参与2024腾讯技术创作特训营最新征文,快来和我瓜分大奖!

0 人点赞