[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
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)
以下是转换结果的示例:
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 提示技术。这是一种奇特的说法,我们应该提供我们想要的模型输出的示例。现在我们这样写模型提示:
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 模型)定义输出模式。
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。
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 构建的已完成提示如下所示:
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 进行信息提取。
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 来完成此任务。
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)基础环境
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 文件:
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)创建集群
现在,您可以使用以下命令预配集群:
# Provision the cluster and update ~/.kube/config
paka cluster up -f cluster.yaml -u
上述命令将创建具有指定配置的新 EKS 集群。它还将使用新的集群信息更新 ~/.kube/config 文件。Paka 从 HuggingFace 中心下载 llama2-7b 模型并将其部署到集群。
4)部署服务
现在,我们想将 FastAPI 应用部署到集群。我们可以通过运行以下命令来执行此操作:
# 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。我们可以通过运行以下命令来执行此操作:
paka function list
如果所有步骤都成功,则该函数应显示在标记为“READY”的列表中。默认情况下,可通过公共 REST API 终结点访问该函数,其格式通常类似于 http://invoice-extraction.default.50.112.90.64.sslip.io。
您可以通过使用 curl 或其他 HTTP 客户端向端点发送 POST 请求来测试 API。下面是一个使用 curl 的示例:
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
如果发票提取成功,响应将显示结构化数据,如下所示:
{"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,并启用了水平扩展。