代码 | 使用本地大模型从文本中提取结构化信息
先说个场景:手头积压了一堆电子发片PDF,想找个法子自动把它们的关键信息扒下来。比如,从一张发片里,我们希望能提取出类似这样的结构化数据:开票日期、应税货物名称、价税合计(大写)、税率、备注。说白了,就是要让AI学会“看”发片。
一、需求
现在手头有很多个电子发片PDF文件,目标很明确——用自动化工具帮我们批量提取出格式化的发片信息。举个例子,从一张发片里,我们希望得到这样一份字典数据:
DICT_DATA = {
"开票日期": "2023年01月06日",
"应税货物(或服务)名称": "*信息技术服务*技术服务费",
"价税合计(大写)": "",
"税率": "6%",
"备注": "230106163474406331"
}
这活儿如果人工做,几百张发片下来,眼睛得花。下面直接上方案。
二、准备工作
2.1 安装ollama
先到官网
https://ollama.com/
2.2 下载大模型
ollama目前支持的主流模型不少,像阿里的qwen、qwen2,Meta的llama3等等。以llama3为例,根据自己电脑的显存性能选择对应版本。如果拿不准选哪个,那就先下个试试,不合适再删,反正折腾的成本不大。
打开命令行(Windows是cmd,Mac是terminal),确保网络畅通,执行下载命令:
ollama run llama3
稍等片刻,等
llama3:8b
2.3 安装python包
要在Python中调用ollama服务,需要装一个配套的包。命令行里执行:
pip3 install ollama
2.4 启动ollama服务
在Python调用之前,得先把本地ollama服务跑起来。命令行里执行:
ollama serve
如果看到类似下面的日志输出,说明服务已经成功启动,默认监听在127.0.0.1:11434端口:
2024/06/14 14:52:24 routes.go:1011: INFO server config env="map[OLLAMA_DEBUG:false ... OLLAMA_HOST:http://127.0.0.1:11434 ...]"
time=2024-06-14T14:52:24.742+08:00 level=INFO source=images.go:725 msg="total blobs: 18"
time=2024-06-14T14:52:24.742+08:00 level=INFO source=images.go:732 msg="total unused blobs removed: 0"
time=2024-06-14T14:52:24.743+08:00 level=INFO source=routes.go:1057 msg="Listening on 127.0.0.1:11434 (version 0.1.44)"
...
time=2024-06-14T14:52:24.796+08:00 level=INFO source=types.go:71 msg="inference compute" id=0 library=metal compute="" driver=0.0 name="" total="72.0 GiB" a vailable="72.0 GiB"
三、实验
3.1 读取pdf
先拿一张发片PDF试试手。用cntext库先读入文本:
import cntext as ct
text = ct.read_pdf('data/1.pdf')
print(ct.__version__)
text
输出结果:
2.1.2
' 机器编号:北京增值税电子普通发片发片代码:n发片号码:n开票日期:2023年01月06日n校 验 码:n购n买n方名 称: 哈尔滨所以然信息技术有限公司n密n码n区030898/5<32>*/0*440/63+79*08n纳税人识别号:91230109MABT7KBC4M /<54<1*6+49<-*+*>7<-8*04<+01n地 址、电 话:68+160026-45904*2<+3+15503>2n开户行及账号:98*2/*-*480145+-19*0917-1*61n货物或应税劳务、服务名称 规格型号 单 位 数 量 单 价 金 额 税率 税 额n*信息技术服务*技术服务费 1248.113208 248.11 6% 14.89n合 计 ¥248.11 ¥14.89n价税合计(大写)n 贰佰陆拾叁元整 (小写)¥263.00n销n售n方名 称: 北京n备 注230106163474406331n纳税人识别号:n地 址、电 话: n开户行及账号: n销售方:(章)'
可以看到,原始PDF读出来的文本比较杂乱,信息散布在各处,这正好是发挥大模型语义理解能力的地方。
3.2 提取信息
我们选用llama3:8b模型,然后设计一个提示词。实验里用的提示如:
“提取TEXT中的关键信息,返回DICT_DATA,DICT_DATA为dict数据格式,所含关键词依次为"开票日期", "应税货物(或服务)名称", "价税合计(大写)", "税率", "备注"; 结果只显示DICT_DATA。”
实际调用代码:
import ollama
response = ollama.chat(model='llama3:8b', messages=[
{
'role': 'user',
'content': f'提取TEXT中的关键信息,返回DICT_DATA, DICT_DATA为dict数据格式,所含关键词依次为"开票日期", "应税货物(或服务)名称", "价税合计(大写)", "税率", "备注"; 结果只显示DICT_DATA。TEXT: {text1}',
},
])
result = response['message']['content']
result
运行结果:
'DICT_DATA = {n "开票日期": "2023年01月06日",n "应税货物(或服务)名称": "*信息技术服务*技术服务费",n "价税合计(大写)": "",n "税率": "6%",n "备注": "230106163474406331"n}'
从结果看,大模型准确提取到了我们需要的信息,耗时大约10秒。不过有一点要注意:有时大模型会返回带多余解释的文本,比如:
'Here is the extracted key information in dictionary format:\n\n```\n{\n "开票日期": "2023年01月06日",\n "应税货物(或服务)名称": "*信息技术服务*技术服务费",\n "价税合计(大写)": "贰佰陆拾叁元整",\n "税率": "6%",\n "备注": "230106163474406331"\n}\n```\n\nLet me know if you ha ve any further requests! ?'
所以需要做一步后处理,把真正的字典部分从回答中提取出来:
import re
result = response['message']['content']
result = [r for r in re.split('```|DICT_DATA = ', result) if '{' in r][0]
print(type(eval(result)))
print(eval(result))
输出:
{'开票日期': '2023年01月06日',
'应税货物(或服务)名称': '*信息技术服务*技术服务费',
'价税合计(大写)': '贰佰陆拾叁元整',
'税率': '6%',
'备注': '230106163474406331'}
3.3 封装成函数extract_info
实验跑通了,下一步就是把流程封装成通用函数:
import ollama
import re
def extract_info(text):
response = ollama.chat(model='llama3:8b', stream=False, messages=[
{
'role': 'user',
'content': f'提取TEXT中的关键信息,返回DICT_DATA, DICT_DATA为dict数据格式,所含关键词依次为"开票日期", "应税货物(或服务)名称", "价税合计(大写)", "税率", "备注"; 结果只显示DICT_DATA。TEXT: {text}',
},
])
result = response['message']['content']
result = [r for r in re.split('```|DICT_DATA = ', result) if '{' in r][0]
return eval(result)
result = extract_info(text)
result
运行结果:
{'开票日期': '2023 02 14',
'应税货物(或服务)名称': '*家用厨房电器具*米家 小米电热水 MJDSH03YM',
'价税合计(大写)': '壹佰贰拾叁圆玖角玖分',
'税率': '13%',
'备注': None}
3.4 批量提取
假设data文件夹里躺着成百上千张发片(虽然本例只有一张),批量提取并保存为csv文件:
%%time
import os
import cntext as ct
import pandas as pd
#获取data内所有pdf的路径
pdf_files = [f'data/{file}' for file in os.listdir('data') if '.pdf' in file]
dict_datas = []
for pdf_file in pdf_files:
pdf_text = ct.read_pdf(pdf_file)
dict_data = extract_info(pdf_text)
dict_datas.append(dict_data)
df = pd.DataFrame(dict_datas)
df
运行结果:
CPU times: user 32 ms, sys: 2.17 ms, total: 15.2 ms
Wall time: 3.8 s

四、讨论
必须得说,本文只用了一张发片做实验,实际跑起来准确率可没这么高。最容易出错的字段是销售方纳税识别号(案例中没有展示这个字段的识别),问题出在ct.read_pdf读出来的文本比较杂乱,对大模型的语义理解是个不小的挑战。好在现在的大模型已经支持文本、图片、音频、视频甚至网址输入,所以这个问题在不久的将来应该能迎刃而解。
另外一点需要留意:大模型对每个输入给出的是概率最大的回答,所以提取数据时存在一定的错误识别风险。要降低这个风险,尽量选那些特征特别特殊、显眼的信息。比如价税合计(大写),因为它用的是中文大写数字,在所有文本中最为醒目、最特别,大模型在处理这类信息时会下意识地给更高权重,回答的准确率自然就上来了。
-
- 关于宇宙的好的网名有哪些
- 角色扮演 | 1
- 网名