简介本资源面向自然语言处理初学者与信息抽取方向开发者提供一套基于PaddleNLP框架的完整中文实体识别项目实践。内容围绕Doccano标注工具构建中文实体识别数据集并借助UIE-base预训练模型进行微调训练最终实现从非结构化文本中自动提取姓名、地名、机构名等关键信息可应用于知识图谱构建、问答系统等场景。压缩包共23个文件约74KB以Python脚本为主涵盖数据标注、模型微调与推理部署全流程另含txt说明文档、jsonl标注数据、yml配置、Dockerfile及docx附赠资料便于快速复现与二次开发。目前已有134人学习下载适合希望掌握信息抽取落地流程的读者参考借鉴。1. 从一堆简历和合同里把姓名抠出来PaddleNLP Doccano UIE-base 到底在解决什么手上有一批中文简历、合同或者工单需要把里面的人名、公司、职位、金额这些字段自动抽出来人工一条条看显然不现实。这个项目标题讲的就是一条完整的落地链路用 Doccano 标注工具构建中文实体识别数据集用 PaddleNLP 里的 UIE-base 预训练模型做微调训练最后部署成能从非结构化文本里自动提取姓名的服务。它解决的不是模型能不能跑的问题而是从零标注到上线推理这条链路上每一步怎么接起来。适合两类人一类是手里有业务文本、想快速验证信息抽取可行性的工程师另一类是已经用过 BERT 类模型做 NER但想换成 UIE 这种统一建模方式、少写几套代码的开发者。整条链路里Doccano 负责把标注这件事变得可协作UIE-base 负责把抽取统一成生成式 span 预测PaddleNLP 负责把训练和部署的工程细节收口。下面按标注、数据转换、微调、部署、避坑的顺序拆开讲。2. Doccano 标注中文实体数据集从安装到导出可训练格式2.1 为什么选 Doccano 而不是自己写标注脚本做中文实体识别标注环节最容易翻车。自己写个 Excel 模板让业务同学填最后拿到的往往是格式不统一、边界对不齐、实体类型拼写不一致的脏数据。Doccano 的价值在于它把标注界面、实体类型管理、多人协作和导出格式都标准化了。它支持序列标注、序列到序列、文本分类等任务做 NER 时选序列标注项目类型界面上直接划词选实体导出时能拿到 JSONL 格式每行是一条文本加对应的标签列表。常见做法是用 Docker 起一个 Doccano 服务团队通过浏览器访问标注进度和一致性都能看到。相比 Label StudioDoccano 更轻部署依赖少对中文分词边界没有额外要求适合中小规模数据集快速起步。2.2 用 Docker 把 Doccano 跑起来的最小命令Doccano 官方推荐用 Docker 部署避免 Python 版本和依赖冲突。下面这套命令在 Linux 或 macOS 上可以直接复现Windows 用 WSL2 同样可行。# 拉取 doccano 镜像这里用官方镜像名 docker pull doccano/doccano:latest # 启动容器映射 8000 端口挂载数据卷防止容器删除后标注丢失 docker run -d --name doccano \ -p 8000:8000 \ -v doccano_data:/data \ -e ADMIN_USERNAMEadmin \ -e ADMIN_PASSWORDadmin123 \ -e ADMIN_EMAILadminexample.com \ doccano/doccano:latest # 查看容器状态确认没有反复重启 docker ps -a | grep doccano逻辑说明-v doccano_data:/data是关键Doccano 的 SQLite 数据库和上传文件都放在这个卷里不挂载的话容器一删标注全没。ADMIN_USERNAME等环境变量只在首次初始化时生效后续改密码要在界面里操作。参数方面端口 8000 如果被占用把-p 8000:8000改成-p 9000:8000浏览器访问 9000 即可。启动后访问http://localhost:8000用上面设的账号登录。2.3 建项目、定实体类型、导入原始文本登录后点创建项目项目类型选序列标注然后进入标签页面添加实体类型。做姓名抽取至少要有PERSON如果同时抽公司、职位就加ORG、TITLE。标签名建议全大写英文避免中文标签在后续转换脚本里出现编码问题。导入数据时Doccano 支持 JSONL、CoNLL、纯文本等格式。最省事的是纯文本一行一条样本。如果原始数据是 CSV先用 Python 转成一行一条的 txtimport pandas as pd # 假设原始 CSV 有一列叫 content df pd.read_csv(raw_texts.csv) with open(for_doccano.txt, w, encodingutf-8) as f: for text in df[content].dropna(): # 去掉换行保证一行一条 clean text.replace(\n, ).strip() if clean: f.write(clean \n)逻辑说明Doccano 导入纯文本时按行切分所以必须保证每条样本内部没有换行。参数上dropna()去掉空文本strip()去首尾空格避免标注时划词划到空白。导入后在数据集页面就能看到待标注文本点进去开始划词。2.4 标注完成后导出 JSONL 并检查标签分布标注完成后在导出数据集里选 JSONLDoccano 里通常叫 JSONL 或 JSONL(TextLabel)下载得到admin.jsonl。每行结构大致是{text: ..., labels: [[start, end, PERSON], ...]}其中 start 和 end 是字符级偏移左闭右开。拿到后先做一次标签分布统计确认没有漏标或类型写错import json from collections import Counter counter Counter() with open(admin.jsonl, encodingutf-8) as f: for line in f: item json.loads(line) for _, _, label in item[labels]: counter[label] 1 print(counter) # 期望输出类似 Counter({PERSON: 320, ORG: 150})逻辑说明这一步是血泪经验很多人标完直接拿去训练结果发现某个实体类型只有个位数样本模型根本学不会。参数上如果某个标签数量低于 50要么补标要么在训练时考虑合并或放弃该类型。另外要检查labels里有没有 start 大于 end 的异常记录有的话在转换阶段直接过滤。3. 把 Doccano 的 JSONL 转成 UIE 训练格式偏移对齐与负样本构造3.1 UIE 的输入输出长什么样UIEUniversal Information Extractionbase 是 PaddleNLP 里一个统一建模信息抽取的预训练模型。它和传统 BERTCRF 的 NER 不一样传统做法是给每个 token 打 BIO 标签UIE 是把抽取任务转成prompt 文本的生成式 span 预测。训练时输入形如姓名: 张三的公司在北京市模型要预测出张三这个 span。也就是说实体类型通过 prompt 前缀告诉模型模型输出的是原文里的片段。这种设计的好处是同一套模型能同时做实体、关系、事件抽取换任务只需要换 prompt不用改模型结构。3.2 转换脚本从 JSONL 到 UIE 微调数据PaddleNLP 的 UIE 微调数据格式是每行一个字典包含text、prompt、result_list等字段。下面这个脚本把 Doccano 的字符偏移转成 UIE 需要的格式import json # 实体类型到 prompt 的映射prompt 用中文描述和推理时保持一致 PROMPT_MAP { PERSON: 姓名, ORG: 公司, TITLE: 职位, } def convert(input_path, output_path): with open(input_path, encodingutf-8) as fin, \ open(output_path, w, encodingutf-8) as fout: for line in fin: item json.loads(line) text item[text] # 按实体类型分组同一类型放在一个 prompt 下 grouped {} for start, end, label in item[labels]: if label not in PROMPT_MAP: continue if start end or end len(text): continue # 过滤异常偏移 grouped.setdefault(label, []).append({ text: text[start:end], start: start, end: end, }) for label, spans in grouped.items(): sample { text: text, prompt: PROMPT_MAP[label], result_list: spans, } fout.write(json.dumps(sample, ensure_asciiFalse) \n) convert(admin.jsonl, uie_train.json)逻辑说明Doccano 的偏移是字符级Python 字符串切片也是字符级所以直接text[start:end]就能拿到实体文本不需要像 token 级标注那样做 offset mapping。参数上PROMPT_MAP必须和推理时用的 prompt 完全一致否则模型学到的映射对不上。start end和end len(text)两个判断是后悔药防止标注时手滑产生的脏数据把训练搞崩。3.3 负样本怎么加不是所有文本都有实体UIE 训练时如果只给正样本模型会倾向于在任何文本里都硬抽一个 span 出来。常见做法是加入一定比例的负样本即result_list为空列表的样本。可以从原始文本里随机抽一些不含实体的句子构造成{text: ..., prompt: 姓名, result_list: []}。比例一般控制在正样本的 10% 到 20%。参数上负样本太多会让模型过于保守漏抽增加太少则误抽明显。我一般先用 15% 跑一版看验证集上的 precision 和 recall 再调。3.4 划分训练集和验证集时注意实体分布直接随机切分可能导致验证集里某个实体类型一个都没有。更稳的做法是按实体类型分层抽样import json import random from collections import defaultdict samples [json.loads(l) for l in open(uie_train.json, encodingutf-8)] by_prompt defaultdict(list) for s in samples: by_prompt[s[prompt]].append(s) train, dev [], [] for prompt, items in by_prompt.items(): random.shuffle(items) cut int(len(items) * 0.9) train.extend(items[:cut]) dev.extend(items[cut:]) random.shuffle(train) random.shuffle(dev) with open(train.json, w, encodingutf-8) as f: for s in train: f.write(json.dumps(s, ensure_asciiFalse) \n) with open(dev.json, w, encodingutf-8) as f: for s in dev: f.write(json.dumps(s, ensure_asciiFalse) \n)逻辑说明按 prompt 分组后再切分保证每个实体类型在训练集和验证集里都有。参数上90/10 是中小数据集的常用比例如果样本量低于 1000可以改成 80/20让验证集更有统计意义。4. UIE-base 微调训练PaddleNLP 命令、关键参数与显存控制4.1 用 PaddleNLP 自带脚本启动微调PaddleNLP 在examples/information_extraction/uie目录下提供了微调脚本。假设已经装好paddlenlp和paddlepaddle-gpu进入该目录后执行python finetune.py \ --train_path ./data/train.json \ --dev_path ./data/dev.json \ --save_dir ./checkpoint \ --learning_rate 1e-5 \ --batch_size 16 \ --max_seq_len 256 \ --epochs 20 \ --model uie-base \ --seed 42 \ --logging_steps 10 \ --eval_steps 100 \ --device gpu逻辑说明--model uie-base指定从 UIE-base 预训练权重开始微调不是从零训练。--max_seq_len 256要覆盖 prompt 加文本的总长度中文简历类文本一般 256 够用合同类可能要到 512。--learning_rate 1e-5是微调的典型量级UIE 对学习率比较敏感超过 5e-5 容易把预训练学到的语言知识冲掉。--batch_size 16在 16G 显存上跑 uie-base 加 256 长度基本安全显存不够就降到 8 并配合梯度累积。4.2 关键参数怎么调学习率、batch size、max_seq_len学习率是微调里最玄学的参数。UIE-base 的推荐范围是 1e-5 到 3e-5我一般从 1e-5 起步如果训练 loss 下降太慢再往上加。batch size 受显存限制但太小会让梯度噪声大可以用--gradient_accumulation_steps模拟大 batch。max_seq_len 直接决定显存占用和推理速度原则是覆盖 95% 以上样本的长度而不是无脑拉满。可以先统计训练集文本长度分布import json import numpy as np lengths [] for line in open(train.json, encodingutf-8): item json.loads(line) # prompt 加文本的大致长度 lengths.append(len(item[prompt]) len(item[text])) print(95分位长度:, int(np.percentile(lengths, 95))) print(最大长度:, max(lengths))逻辑说明如果 95 分位是 200那 max_seq_len 设 256 就够设 512 只会浪费显存。参数上超过 max_seq_len 的样本会被截断截断位置如果正好切掉实体这条样本就废了所以宁可稍微设大一点也不要让实体被截断。4.3 训练过程中看什么指标微调脚本一般会输出 loss 和验证集上的 precision、recall、F1。重点看验证集 F1 是否在上升以及训练 loss 和验证 loss 的差距。如果训练 loss 持续下降但验证 F1 停滞甚至下降说明过拟合可以减小 epoch、加 dropout 或增加数据。如果两者都下不去先检查数据格式和 prompt 映射是否一致这是最常见的翻车点。--eval_steps 100表示每 100 步评估一次小数据集可以设小一点比如 50方便早停。4.4 显存不够时的三个降级方案16G 显存跑 uie-base 加 512 长度、batch size 16 可能 OOM。降级顺序是先把 batch size 降到 8再加--gradient_accumulation_steps 2保持等效 batch还不够就把 max_seq_len 降到 384 或 256最后才考虑换 uie-tiny 或 uie-micro但精度会掉适合对速度要求高、实体类型简单的场景。注意 gradient accumulation 会拖慢训练速度但不会影响最终效果太多。5. 部署推理把微调后的 UIE 模型接成可调用的抽取服务5.1 加载 checkpoint 做单条推理训练完成后./checkpoint目录下有model_state.pdparams和model_config.json。用 PaddleNLP 的 Taskflow 可以最快验证效果from paddlenlp import Taskflow # 指定自己微调后的模型路径 schema [姓名, 公司, 职位] ie Taskflow(information_extraction, schemaschema, task_path./checkpoint, device_id0) text 张三在字节跳动担任算法工程师李四在腾讯做产品经理。 result ie(text) print(result)逻辑说明schema里的字段必须和训练时的 prompt 一致顺序无所谓但名称要对上。task_path指向 checkpoint 目录Taskflow 会自动加载模型结构和权重。device_id0用 GPU设 -1 用 CPU。输出是每个 schema 字段对应的实体列表包含 text、start、end、probability。参数上如果发现抽取结果里实体边界不对优先检查训练数据的偏移是否准确而不是调推理参数。5.2 批量推理和性能优化单条推理方便调试生产环境要批量处理。Taskflow 支持传入列表texts [张三在字节跳动担任算法工程师。, 李四在腾讯做产品经理。] results ie(texts) for t, r in zip(texts, results): print(t, r)逻辑说明批量推理时 PaddleNLP 会自动做 padding 和 batch 组装吞吐比逐条高很多。参数上可以通过ie.set_model_max_length(256)控制最大长度和训练时保持一致。如果 QPS 要求高可以考虑用 Paddle Inference 或 ONNX 导出做加速但那是另一个话题先用 Taskflow 把链路跑通。5.3 用 FastAPI 包一层 HTTP 接口要把抽取能力给其他系统调用包一层 HTTP 接口最直接from fastapi import FastAPI from pydantic import BaseModel from paddlenlp import Taskflow app FastAPI() ie Taskflow(information_extraction, schema[姓名, 公司, 职位], task_path./checkpoint, device_id0) class Req(BaseModel): text: str app.post(/extract) def extract(req: Req): return {result: ie(req.text)} # 启动: uvicorn main:app --host 0.0.0.0 --port 8080逻辑说明模型在服务启动时加载一次常驻显存避免每次请求都重新加载。参数上--host 0.0.0.0让服务对外可访问生产环境前面一般再加 Nginx 做负载和限流。注意 FastAPI 默认单 worker并发高时要用 gunicorn 加 uvicorn worker但每个 worker 会各自加载一份模型显存要算够。6. 避坑与排查标注、转换、训练、部署里最容易翻车的五件事6.1 现象训练 loss 正常下降但推理时抽不出任何实体原因训练时的 prompt 和推理时的 schema 不一致。比如训练用姓名推理写成人名模型没见过这个 prompt输出为空。解决把训练脚本里的PROMPT_MAP和推理schema放在同一个配置文件里两边引用同一份杜绝手写不一致。6.2 现象实体边界总是多一个字或少一个字原因Doccano 标注时划词边界没对齐或者导出格式的偏移是 token 级而非字符级。解决确认导出的是 JSONL 字符偏移格式转换脚本里打印几条text[start:end]人工核对。如果原始数据里有全角空格、零宽字符先做清洗再标注。6.3 现象验证集 F1 很高上线后误抽严重原因验证集和线上数据分布不一致或者负样本太少。解决从线上真实文本里抽一批做验证集并补充负样本重新训练。参数上负样本比例从 15% 提到 25% 再试观察 precision 变化。6.4 现象Doccano 容器重启后标注数据不见了原因启动时没挂载数据卷数据存在容器内部。解决用-v doccano_data:/data重新起容器如果已经有数据在旧容器里先docker cp拷出来再迁移。以后所有有状态服务都要挂卷这是基本习惯。6.5 现象微调时显存 OOM报错指向 attention原因max_seq_len 或 batch size 超过显存容量。解决按第 4.4 节的降级顺序处理先降 batch 加梯度累积再降长度。注意 UIE 的 prompt 也占长度统计长度时要把 prompt 算进去否则设的 max_seq_len 会比预期更早截断实体。7. 进阶用 prompt 组合和阈值调优把抽取精度再抬一档微调跑通之后真正拉开效果差距的往往不是模型结构而是 prompt 设计和后处理阈值。UIE 的一个优势是同一份文本可以用不同 prompt 抽不同维度比如姓名和曾用名分开抽再在业务层合并。我一般会准备一组同义 prompt比如姓名和人名训练时都覆盖推理时取并集再去重召回会明显好于单一 prompt。另一个技巧是调 probability 阈值Taskflow 输出里每个实体带 probability默认阈值较低误抽多把阈值从默认值提到 0.7 左右precision 上升recall 略降具体值要在验证集上扫一遍。# 阈值过滤示例 def filter_by_prob(result, threshold0.7): filtered {} for label, spans in result.items(): kept [s for s in spans if s.get(probability, 1.0) threshold] if kept: filtered[label] kept return filtered raw ie(张三在字节跳动担任算法工程师。) print(filter_by_prob(raw, 0.7))逻辑说明probability 是模型对 span 的置信度阈值过滤是最低成本的后处理。参数上阈值不是越高越好0.9 以上会漏掉很多正确实体建议在验证集上画 precision-recall 曲线选拐点。另外如果同一实体被多个 prompt 抽出按 start/end 去重保留 probability 最高的那条。还有一个容易被忽略的点UIE 对文本长度敏感长文本里实体分散时可以按句号切分后逐句抽取再合并比整段塞进去效果更稳。切分时注意保留偏移合并时把句子级偏移加回全局偏移否则 start/end 对不上原文。这套组合拳打下来姓名抽取在简历类文本上 F1 通常能比裸微调高几个点。我自己踩过的最大坑是早期没做 prompt 一致性检查训练和推理各写各的白白浪费了两天排查模型后来把所有 prompt 收进一个常量文件再没出过这类问题。希望帮到你。本文还有配套的精品资源点击获取