ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

PaddleNLP+UIE-base中文信息抽取实战:从Doccano标注到部署

PaddleNLP+UIE-base中文信息抽取实战:从Doccano标注到部署 简介本资源面向自然语言处理初学者与信息抽取方向开发者提供一套基于PaddleNLP框架的完整中文实体识别项目实践解决从非结构化文本中自动提取姓名、地名、机构名等实体的工程落地问题。包内共23个文件以Python脚本、txt数据与说明文档、jsonl标注数据、yml配置及Dockerfile为主涵盖数据标注、模型微调、推理部署等环节压缩包约74KB结构紧凑便于快速上手。已有134人学习下载适合希望掌握UIE-base微调流程与Doccano标注方法的读者参考。资源包含训练与测试脚本、示例数据、使用笔记及部署配置可帮助读者理解从数据集构建到模型上线的完整链路并借助说明文档与附赠资料排查常见问题具备较强的实操参考价值。1. 从一堆简历和合同里抽字段PaddleNLP Doccano UIE-base 到底能干什么手里有一批中文简历、合同或者工单想把里面的姓名、公司、时间、金额自动抠出来这件事在 2024 年已经不需要从零训 BERT 了。PaddleNLP 里的 UIE-base 把「信息抽取」变成了一个填空任务你告诉它要抽什么它就从非结构化文本里把对应的片段找出来。配合 Doccano 做标注整个链路是「标注 → 导出 → 微调 → 部署」一个人两三天能跑通。这套方案适合谁适合手上有几百到几千条中文文本、需要抽取固定几类实体、又不想养一个算法团队的小团队或个人开发者。它不解决开放域任意关系抽取也不解决超长文档的跨段推理但对「简历抽姓名电话」「合同抽甲乙方和金额」这类任务UIE-base 微调后的效果足够落地。下面按我实际做过的顺序把每一步的参数和坑讲清楚。2. 用 Doccano 构建中文实体识别数据集从安装到导出2.1 Doccano 的安装方式与 Windows 环境下的部署选择Doccano 是一个开源的文本标注工具支持实体、关系、分类三种标注模式。标题里提到的是实体识别数据集所以重点用它的序列标注功能。安装方式常见有三种pip 直接装、Docker 拉镜像、源码跑。我一般推荐 Docker因为 Doccano 依赖 Django 和 PostgreSQLpip 装容易在 Windows 上卡在 psycopg2 编译。Windows 环境下部署 Doccano 是热搜里反复出现的问题核心矛盾是 Docker Desktop 需要 WSL2而很多人的 Windows 版本或虚拟化设置没开。先确认两件事BIOS 里虚拟化打开系统设置里「虚拟机平台」和「适用于 Linux 的 Windows 子系统」两个功能勾上。然后装 Docker Desktop重启再执行docker pull doccano/doccano docker container create --name doccano \ -e ADMIN_USERNAMEadmin \ -e ADMIN_EMAILadminexample.com \ -e ADMIN_PASSWORDpassword \ -v doccano-db:/data \ -p 8000:8000 doccano/doccano docker container start doccano这段命令的逻辑先拉官方镜像再创建一个名为 doccano 的容器通过环境变量设定管理员账号把数据卷挂到/data保证标注数据不随容器删除而丢失最后映射 8000 端口。参数上ADMIN_PASSWORD至少 8 位否则容器启动会报校验失败-v doccano-db:/data是命名卷比绑定宿主机目录更省事Windows 路径映射经常出权限问题。启动后浏览器开http://localhost:8000用设定的账号登录。如果 Docker 实在跑不起来退而求其次用 pippython -m venv doccano-env doccano-env\Scripts\activate pip install doccano doccano init doccano createuser --username admin --password password doccano webserver --port 8000注意 pip 方式默认用 SQLite数据量超过几千条会明显变慢而且 Windows 上doccano init偶尔因为路径含中文失败把项目放在纯英文路径下能避开。2.2 标注 schema 的设计实体类别不是越多越好登录后第一件事是建项目选「序列标注」。然后定义标签。这里有个血泪经验标签体系一旦定下来后面改的成本很高因为已标注数据要重新映射。我一般遵循三个原则。第一类别数控制在 5 到 15 之间。UIE-base 的抽取能力对类别数量不敏感但标注一致性对类别数量极其敏感。超过 15 类标注员开始混淆「公司名」和「机构名」这种边界。第二标签名用英文短词别用中文。Doccano 导出后标签名会直接进训练脚本中文标签在某些编码环节会翻车。比如抽简历用NAME、COMPANY、TITLE、EDU、TIME而不是「姓名」「公司」。第三先标 50 条做一致性检查。让两个人各标 50 条算一下同一实体的边界是否一致。如果「北京大学」有人标成EDU有人标成ORG说明 schema 定义有歧义先改定义再继续。2.3 标注操作与导出格式JSONL 和 UIE 格式的转换Doccano 的标注界面里选中文本后点标签即可。快捷键是a选标签、w确认熟练后一条短文本 10 秒内标完。标完在「导出数据」里选 JSONL得到每行一个 JSON 的文件结构大致是{text: 张三在2020年加入阿里巴巴担任产品经理, labels: [[0, 2, NAME], [6, 10, TIME], [12, 16, COMPANY], [18, 22, TITLE]]}labels里每个元素是[起始位置, 结束位置, 标签名]注意结束位置是开区间。这个格式不能直接喂给 UIE需要转成 UIE 的 prompt 格式。UIE 的训练数据要求把实体转成「文本 提示 答案」的三元组转换脚本如下import json def doccano_to_uie(jsonl_path, out_path, schema): # schema 是标签名到中文提示的映射如 {NAME: 姓名} with open(jsonl_path, r, encodingutf-8) as fin, \ open(out_path, w, encodingutf-8) as fout: for line in fin: item json.loads(line) text item[text] # 按标签分组同一标签的多个实体用逗号连接 grouped {} for start, end, label in item[labels]: grouped.setdefault(label, []).append(text[start:end]) for label, prompt in schema.items(): entities grouped.get(label, []) # UIE 格式prompt 和答案用 \t 分隔无实体时答案为空 record { text: text, prompt: prompt, answer: 、.join(entities) if entities else } fout.write(json.dumps(record, ensure_asciiFalse) \n) schema {NAME: 姓名, TIME: 时间, COMPANY: 公司, TITLE: 职位} doccano_to_uie(export.jsonl, train_uie.jsonl, schema)逻辑说明UIE 的核心思想是把抽取任务转成「给定 prompt从 text 里找答案」。所以每条原始文本会按 schema 展开成多条训练样本每条对应一个 prompt。参数上answer里多个实体用顿号连接是 UIE 官方推荐的写法训练时模型学会一次输出多个片段。注意无实体时answer为空字符串这类负样本要保留否则模型会倾向于乱抽。转换后检查一下样本数原始 500 条文本、4 个标签应该得到 2000 条 UIE 样本。3. UIE-base 微调训练数据格式、超参和显存控制3.1 PaddleNLP 环境搭建与 UIE-base 模型加载PaddleNLP 的安装比想象中简单但版本匹配是玄学。我一般用 GPU 环境先装 PaddlePaddle 再装 PaddleNLPpython -m pip install paddlepaddle-gpu2.6.1 -i https://mirror.baidu.com/pypi/simple pip install paddlenlp2.7.0版本号要对应PaddleNLP 2.7 配 PaddlePaddle 2.6 是稳的。装完验证from paddlenlp import Taskflow schema [姓名, 时间, 公司, 职位] ie Taskflow(information_extraction, schemaschema, modeluie-base) print(ie(张三在2020年加入阿里巴巴担任产品经理))这段代码加载的是零样本 UIE-base不微调也能抽但准确率取决于 schema 和文本领域的匹配度。输出是一个列表每个元素对应一个 schema 项里面是抽到的文本和起止位置。如果这一步报显存不足说明 GPU 被其他进程占了或者模型加载时默认用了 FP32后面微调时可以用 FP16 压。3.2 微调数据格式与训练脚本的关键参数PaddleNLP 的 UIE 微调脚本在examples/information_extraction/uie目录下。数据格式要求每行一个 JSON字段是text、prompt、answer正是上一章转换出来的格式。把训练集和验证集按 9:1 切分放到data/目录下。训练命令python -u -m paddle.distributed.launch --gpus 0 finetune.py \ --device gpu \ --logging_steps 10 \ --save_steps 100 \ --eval_steps 100 \ --seed 42 \ --model_name_or_path uie-base \ --output_dir ./checkpoint \ --train_path data/train.jsonl \ --dev_path data/dev.jsonl \ --max_seq_len 512 \ --per_device_train_batch_size 16 \ --per_device_eval_batch_size 16 \ --num_train_epochs 20 \ --learning_rate 1e-5 \ --do_train \ --do_eval \ --do_export逐个说关键参数。max_seq_len 512是 UIE-base 的上限超过会被截断如果你的文本平均长度超过 400考虑分段或换 uie-base-zh 的长文本版本。per_device_train_batch_size 16在 8G 显存上能跑12G 可以上 32。learning_rate 1e-5是微调 UIE 的甜点值我试过 5e-5 会震荡1e-6 收敛太慢。num_train_epochs 20配合eval_steps 100实际会在验证集指标不再提升时提前停不用怕过拟合。do_export会把最好的 checkpoint 导出成静态图部署时用。训练过程中看eval_f1如果 3 个 epoch 后还在 0.5 以下大概率是数据问题要么标注边界不一致要么 prompt 和实体类型对不上。我遇到过一次把「时间」的 prompt 写成「日期」模型 F1 直接掉 0.2因为预训练时 UIE 见过的是「时间」这个 prompt。3.3 显存不够时的三个降级方案不是所有人都有大显存卡。如果训练时 OOM按顺序试这三个方案。第一降 batch size 到 8同时把learning_rate降到 5e-6用梯度累积补回来。在脚本里加--gradient_accumulation_steps 2等效 batch 还是 16。第二开 FP16。加--fp16参数显存能省 30% 左右但要注意某些算子不支持 FP16 会报错报错就去掉。第三冻结底层。UIE-base 是 12 层 encoder冻结前 6 层只训后 6 层显存降一半但小数据集上效果可能更好因为底层语言知识本来就不需要大改。改法是加载模型后遍历model.encoder.layer[:6]设requires_gradFalse。4. 部署与推理从 checkpoint 到可调用的抽取服务4.1 导出静态图与 Taskflow 加载微调模型训练完的 checkpoint 目录里有model_state.pdparams和配置文件。部署有两种方式动态图直接加载或者导出静态图用 Paddle Inference。动态图简单但慢静态图快但多一步导出。我一般先用动态图验证效果再导静态图上线。动态图加载微调后的模型from paddlenlp import Taskflow schema [姓名, 时间, 公司, 职位] ie Taskflow(information_extraction, schemaschema, task_path./checkpoint/model_best, device_id0) result ie(李四于2019年入职腾讯任高级工程师) print(result)task_path指向model_best目录里面要有model_state.pdparams和model_config.json。如果报 key 不匹配检查训练时的 schema 顺序和推理时是否一致UIE 对 prompt 顺序敏感。静态图导出python export_model.py \ --model_path ./checkpoint/model_best \ --output_path ./export \ --schema 姓名 时间 公司 职位导出后在./export下得到inference.pdmodel和inference.pdiparams用 Paddle Inference 加载单条推理延迟能从动态图的 80ms 降到 20ms 左右。4.2 批量推理与结果后处理实际业务里很少一条一条抽通常是几千条文本批量跑。批量推理要注意两点一是按长度分桶避免 padding 浪费二是设置batch_size别太大UIE 的输出是变长的batch 内不同样本的实体数差异大会导致后处理复杂。from paddlenlp import Taskflow import json ie Taskflow(information_extraction, schema[姓名, 时间, 公司, 职位], task_path./checkpoint/model_best, device_id0) def batch_extract(texts, batch_size8): results [] for i in range(0, len(texts), batch_size): batch texts[i:ibatch_size] # Taskflow 支持 list 输入内部自动 padding batch_result ie(batch) for text, res in zip(batch, batch_result): record {text: text} for item in res: for label, entities in item.items(): record[label] [e[text] for e in entities] results.append(record) return results texts [张三在2020年加入阿里巴巴, 李四于2019年入职腾讯] for r in batch_extract(texts): print(json.dumps(r, ensure_asciiFalse))逻辑说明Taskflow 的ie接受 list 输入内部会做 padding 和 batch 推理。后处理时把每个 schema 项下的实体文本抽出来丢掉位置信息因为业务侧通常只要值。参数上batch_size设 8 到 16 比较稳再大显存吃不消。注意如果某条文本没抽到任何实体res里对应项是空列表后处理要处理这种空值别让下游报 KeyError。4.3 用 FastAPI 包一层 HTTP 接口部署的最后一步是给业务方一个 HTTP 接口。FastAPI 是最省事的from fastapi import FastAPI from pydantic import BaseModel from paddlenlp import Taskflow app FastAPI() ie Taskflow(information_extraction, schema[姓名, 时间, 公司, 职位], task_path./checkpoint/model_best, device_id0) class TextIn(BaseModel): text: str app.post(/extract) def extract(item: TextIn): res ie(item.text) out {} for entry in res: for label, entities in entry.items(): out[label] [e[text] for e in entities] return out启动uvicorn main:app --host 0.0.0.0 --port 8080。注意 Taskflow 实例要在模块级别初始化别放在函数里否则每次请求都重新加载模型延迟爆炸。并发上Paddle 的预测默认单线程QPS 要求高的话用--workers 4起多个进程每个进程独立加载模型显存够就行。5. 避坑与排查标注、训练、部署里最容易翻车的五件事5.1 标注边界不一致导致 F1 卡在 0.6 上不去现象训练 loss 正常下降但验证集 F1 到 0.6 就横盘怎么调学习率都没用。原因标注时实体边界不统一。比如「阿里巴巴集团」有人标全称有人只标「阿里巴巴」「2020年3月」有人标到月有人标到日。UIE 学的是片段匹配边界不一致等于给模型灌了噪声。解决抽 100 条已标注数据让两个人重新标一遍算边界完全一致的比例。低于 90% 就停下来统一规范写清楚「公司名包含集团/有限公司后缀」「时间精确到日」这类规则再重新标。已经标完的数据可以用脚本做一次边界对齐把长实体截断到最短公共子串。5.2 prompt 用词和预训练分布不匹配现象零样本时抽得还行微调后反而变差。原因UIE 预训练时见过的 prompt 是有限集合比如「姓名」「时间」「公司」「职位」「地点」这些高频词。如果你自定义成「人名」「入职时间」「雇主单位」模型需要额外数据才能学会小数据集上直接翻车。解决先用零样本跑一遍看哪些 prompt 能抽出来。能抽出来的直接用原词抽不出来的再考虑自定义。自定义的 prompt 要在训练数据里出现足够多次至少 200 条以上。5.3 Windows 下 Doccano 容器启动后无法访问现象docker ps显示容器在跑但浏览器打不开localhost:8000。原因Windows 上 Docker Desktop 的网络代理设置和宿主机端口映射冲突或者 8000 端口被占用。解决先换端口把-p 8000:8000改成-p 9000:8000。还不行就检查 Docker Desktop 的 Settings → Resources → Network把代理关掉。最后确认防火墙没拦 Docker 的虚拟网卡。5.4 训练时 loss 变 NaN现象前几个 step 正常突然 loss 变成 nan之后全是 nan。原因学习率太大或者数据里有空文本。UIE 对空文本会产生除零梯度爆炸。解决先把学习率降到 5e-6 重跑。同时在数据加载时过滤掉text为空或长度小于 2 的样本。如果还 NaN加梯度裁剪在训练脚本里设max_grad_norm1.0。5.5 部署后单条推理正常批量推理结果错位现象一条一条调接口结果对一次传 10 条返回的结果和输入对不上。原因Taskflow 的 list 输入在内部做了排序或分桶返回顺序不一定和输入顺序一致。解决别依赖返回顺序在输入里带一个id字段返回时把id一起带回来下游按id匹配。或者干脆在服务层做循环一次只传一条用并发换吞吐。6. 把抽取准确率再往上推一档三个我常用的进阶技巧微调完的 UIE-base 在干净数据上 F1 到 0.85 左右是常态想再往上走靠调参空间不大得从数据和推理策略上想办法。下面三个是我实际用过有效的。第一个是「负样本增强」。UIE 的训练数据里无实体的样本answer 为空比例如果太低模型会倾向于乱抽。我一般把负样本比例控制在 20% 到 30%。构造方法很简单拿一批不含目标实体的文本按 schema 展开成 answer 为空的样本混进训练集。注意负样本的文本领域要和正样本一致别拿新闻文本给简历抽取当负样本。第二个是「prompt 集成」。同一个实体类型用多个 prompt 各训一个模型推理时取并集或投票。比如「公司」可以用「公司」「企业」「单位」三个 prompt 分别微调推理时三个模型都跑实体出现两次以上才保留。代价是推理成本翻三倍但 F1 通常能涨 2 到 3 个点。适合对准确率敏感、对延迟不敏感的场景。第三个是「规则后处理兜底」。UIE 抽出来的结果里有些错误是模式化的比如时间格式不统一、公司名带了多余空格。写一层正则做归一化比重新训模型划算得多。我常用的几条规则问题正则处理时间含「年」「月」不统一(\d{4})年(\d{1,2})月?统一成YYYY-MM公司名尾部空格\s$去尾空格姓名含称谓(先生女士金额含「万」「元」(\d(\.\d)?)万?元?统一成数字这张表里的规则不是拍脑袋写的是每次 bad case 分析后往里加的。我习惯每上线一周抽 100 条错误结果归类看有没有新模式有就加规则。规则层和模型层是互补的模型负责召回规则负责精度。最后说一个验证方法别只看整体 F1按实体类型拆开看。我做过一个简历抽取整体 F1 0.87 看着不错拆开发现「职位」只有 0.62因为职位名称太发散「高级工程师」「资深开发」「技术专家」边界模糊。这种就得单独给「职位」加数据或者干脆把职位抽取降级成分类任务。整体指标会骗人分类型指标才告诉你下一步该干什么。这套链路我从标注到上线跑过三遍最大的教训是数据质量决定上限模型和参数只决定你离上限有多远。花在标注规范上的时间最后都会从调参时间里省回来。希望帮到你。本文还有配套的精品资源点击获取
返回列表