
简介本资源面向自然语言处理初学者与信息抽取方向的开发者提供一套基于PaddleNLP框架的完整中文实体识别项目实践。内容围绕Doccano标注工具构建中文实体识别数据集并借助UIE-base预训练模型进行微调训练最终实现从非结构化文本中自动提取姓名、地名、机构名等关键信息覆盖数据准备、标注、审查到模型训练与部署的全流程。压缩包共23个文件约74KB以9个Python脚本为核心配合6个txt说明与数据文件、1个jsonl标注样本、1个json配置、1个yml工作流及Dockerfile等结构紧凑便于快速上手。项目已吸引134人学习适合希望掌握信息抽取落地流程的读者参考。通过微调脚本、推理脚本与部署配置读者可理解UIE模型在中文实体识别任务中的训练细节与工程化思路并借助说明文档与附赠资料排查常见问题为知识图谱构建、问答系统等场景提供可复用的技术方案。1. 从一堆杂乱文本里把姓名抠出来这套 UIE-base 微调方案到底能解决什么手里有一批简历、合同或者客服对话记录想批量把里面的人名、地名、机构名自动抽出来用正则写到一半发现中文姓名的边界根本没法穷举——复姓、少数民族姓名、带职务前缀的称呼混在一起规则越写越像玄学。这个项目就是冲着这类场景来的它把 PaddleNLP 的 UIE-base 预训练模型、Doccano 标注工具和一套可跑的微调脚本打包在一起让你从零标注一小批数据训练出能识别自定义实体类型的信息抽取模型最后通过usemodel.py直接对新的非结构化文本做推理。资源包里能看到finetune.py、doccano.py、usemodel.py三个核心脚本加上data目录下的train.txt、dev.txt、test.txt和sample_index.json还有Dockerfile、requirements.txt、workflows/addr.yml这些部署和流程文件。它适合两类人一是想快速验证「用少量标注数据微调 UIE 做垂直领域实体抽取」是否可行的算法工程师二是需要把信息抽取能力集成到现有业务系统里、但不想从零搭训练管线的后端开发。UIE-base 本身是百度的统一信息抽取预训练模型支持实体、关系、事件抽取的统一建模微调之后对中文姓名的识别在垂直语料上通常比通用模型更贴业务。2. 用 Doccano 把原始文本变成 UIE 能吃的标注格式从建项目到导出 jsonl2.1 为什么选 Doccano 而不是直接手写 BIO 标签中文实体识别数据集的构建最原始的做法是拿一个 txt 文件人工在每行后面敲B-PER、I-PER、O这样的标签。这种方式在实体类型少、文本短的时候还能忍一旦要标姓名、地名、机构名、职位四类以上标注一致性会断崖式下跌——不同标注员对「北京市朝阳区」到底算地名还是机构名经常打架。Doccano 的价值在于它把标注界面做成了「选中文本 → 点实体类型」的交互标注结果以 JSONL 存储每条记录包含原文和entities数组每个实体有start_offset、end_offset、label三个字段。这种结构天然适合转成 UIE 需要的 prompt 格式因为 UIE 的训练数据本质上是「文本 实体类型 实体片段」的三元组。项目里的doccano.py就是干这个转换的。它读取 Doccano 导出的 JSONL按 UIE 的模板把每条样本拼成类似文本中的人名是加上实体列表的形式再按比例切分 train/dev/test。sample_index.json则记录了每条样本在原始文件中的索引方便做数据溯源和错误分析。2.2 启动 Doccano 并配置实体标签Windows 环境下部署 Doccano 是热搜里问得最多的问题之一常见做法是用 Docker 跑因为 Doccano 的 Python 包依赖在某些 Windows Python 版本上会卡在chakpy或django的编译环节。项目里附了Dockerfile可以直接构建镜像也可以用官方镜像加挂载目录的方式启动。# 拉取 doccano 官方镜像并启动映射 8000 端口 docker pull doccano/doccano:latest docker run -d --name doccano \ -p 8000:8000 \ -v /host/path/to/doccano_data:/data \ doccano/doccano:latest # 进入容器创建管理员账号 docker exec -it doccano doccano createuser --username admin --password admin123启动后浏览器访问http://localhost:8000用刚创建的账号登录。第一次进后台需要先建一个「序列标注」类型的项目然后在「标签」页面把你要抽的实体类型逐个加进去。比如做姓名抽取就加一个PER标签如果还要抽地名和机构名再加LOC、ORG。标签名建议用英文短码因为后续转 UIE 格式时脚本会直接拿这个标签名做 prompt 拼接中文标签在某些编码环境下容易出问题。提示Doccano 的标注数据默认存在容器内的 SQLite 里如果没挂载/data目录容器一删数据就没了。我一般会在启动前先确认挂载路径的写权限Windows 下用 WSL2 的路径比直接挂C:\更稳。2.3 标注、导出与格式转换标注阶段没什么技术含量但有两个操作细节会影响后续训练效果。第一标注时要尽量覆盖实体的完整边界比如「张三丰」不要只标「张三」否则模型学到的边界会偏。第二对于嵌套实体或者歧义实体宁可不标也不要标错UIE 对噪声标注的容忍度比 BERT 类模型低因为它的 prompt 结构会把错误实体类型直接编码进输入。标完一批数据后在 Doccano 的「导出」页面选 JSONL 格式下载。然后跑项目里的doccano.py做转换# doccano.py 核心逻辑示意读取 doccano 导出的 jsonl 并转成 UIE 格式 import json def convert_doccano_to_uie(input_file, output_file, label_map): with open(input_file, r, encodingutf-8) as fin, \ open(output_file, w, encodingutf-8) as fout: for line in fin: item json.loads(line) text item[text] entities item.get(label, []) # doccano 导出字段可能是 label 或 entities # 按实体类型分组拼成 UIE 的 prompt 格式 prompt_parts [] for label, short in label_map.items(): spans [text[e[0]:e[1]] for e in entities if e[2] label] if spans: prompt_parts.append(f{short}: {、.join(spans)}) if prompt_parts: # UIE 输入格式文本 [SEP] prompt uie_line f{text}\t{ | .join(prompt_parts)} fout.write(uie_line \n) if __name__ __main__: label_map {PER: 人名, LOC: 地名, ORG: 机构名} convert_doccano_to_uie(doccano_export.jsonl, data/train.txt, label_map)这段代码的关键参数是label_map它把 Doccano 里的英文标签映射成 UIE prompt 里的中文描述词。映射词的选择有讲究用「人名」比用「人物」好因为 UIE-base 在预训练阶段见过的 prompt 模板里「人名」出现频率更高微调时收敛更快。转换后的train.txt每行是「原文 制表符 prompt 实体串」dev.txt和test.txt用同样的逻辑切分比例一般按 8:1:1。3. 微调 UIE-basefinetune.py 里的参数怎么设、显存怎么省3.1 UIE-base 的输入构造与损失函数UIE 的微调不是传统的序列标注而是把实体抽取转成「生成式」的文本匹配任务。具体来说finetune.py会把每条样本构造成[CLS] 原文 [SEP] prompt [SEP]的形式prompt 里包含你要抽的实体类型描述模型需要预测原文中哪些 span 对应这些类型。损失函数用的是 span 级别的二分类交叉熵每个 token 位置输出一个是否属于当前实体类型的概率。这种设计的好处是同一套模型可以处理不同实体类型只要改 prompt 就行代价是推理时需要对每个实体类型跑一遍前向实体类型多的时候延迟会线性增长。项目里finetune.py默认用的是 UIE-base 的uie-base配置hidden size 76812 层 Transformer参数量约 118M。如果显存吃紧可以把max_seq_len从 512 降到 256batch size 从 16 降到 8再用梯度累积补回来。3.2 启动微调训练的命令与关键参数# 在项目根目录下启动 UIE-base 微调 python finetune.py \ --train_path data/train.txt \ --dev_path data/dev.txt \ --save_dir ./checkpoint \ --learning_rate 3e-5 \ --batch_size 16 \ --max_seq_len 512 \ --epochs 20 \ --model_name_or_path uie-base \ --seed 42 \ --logging_steps 10 \ --save_steps 100 \ --device gpulearning_rate设 3e-5 是 UIE 微调的常见起点比 BERT 微调常用的 2e-5 略高因为 UIE 的 prompt 结构需要更大的更新幅度来适配新实体类型。batch_size16 在 8GB 显存的卡上跑 512 长度基本是上限再大就要开fp16或者用--gradient_accumulation_steps。epochs20 看起来多但 UIE 在小数据集上通常 10 轮左右 dev F1 就到平台期设 20 是为了配合save_steps做 checkpoint 选择最后取 dev 上最好的那个。训练日志里重点看两个指标eval_f1和eval_precision。如果 F1 在涨但 precision 掉说明模型开始把一些非实体片段也识别成实体常见原因是标注数据里负样本太少或者 prompt 描述词太宽泛。这时候可以回 Doccano 补标一批「看起来像实体但不是」的负样本或者在finetune.py里调高negative_ratio参数。3.3 显存不够时的三种降级方案第一种是换小模型UIE 有uie-micro和uie-mini两个更小的版本参数量分别是 24M 和 12M精度会掉几个点但推理速度翻倍。第二种是开混合精度在命令里加--fp16显存占用能降 30% 左右但要注意某些老卡对 fp16 支持不好会出现 loss 变 NaN 的情况。第三种是冻结底层在finetune.py里把前 6 层 Transformer 的requires_grad设为 False只训顶层和分类头显存能省一半适合数据量小于 2000 条的场景。注意UIE 的max_seq_len不是越大越好。中文姓名抽取的文本通常不超过 200 字设 512 会引入大量 padding反而拖慢训练。我一般先统计训练集文本长度的 95 分位数然后取最近的 2 的幂次作为max_seq_len。4. 推理与部署usemodel.py 怎么调、Dockerfile 里藏了什么4.1 用 usemodel.py 做单条和批量推理训练完成后usemodel.py负责加载 checkpoint 并对新文本做抽取。它的核心逻辑是先加载 UIE 模型和 tokenizer然后对输入文本按实体类型逐个构造 prompt 做前向最后把得分超过阈值的 span 还原成实体片段。# usemodel.py 推理核心加载模型并对单条文本抽取姓名 from paddlenlp import Taskflow # 初始化 UIE 抽取器指定 schema 为要抽取的实体类型 schema [人名] # 对应训练时的 prompt 描述词 ie Taskflow(information_extraction, model./checkpoint/model_best, # 微调后的模型路径 schemaschema) def extract_name(text): result ie(text) # result 是 list of dict每个 dict 的 key 是 schema 里的类型 names [] for item in result: for label, entities in item.items(): for ent in entities: names.append({ text: ent[text], start: ent[start], end: ent[end], probability: ent[probability] }) return names if __name__ __main__: sample 张三丰是武当派创始人后来去了湖北武当山。 print(extract_name(sample))Taskflow的model参数指向checkpoint/model_best这是finetune.py在 dev 上取得最好 F1 时保存的目录。schema必须和训练时的 prompt 描述词完全一致训练时用「人名」推理时就不能写「姓名」否则模型见到的 prompt 分布和训练时对不上召回率会明显下降。probability字段是模型对每个实体的置信度实际业务里可以设一个阈值比如 0.7低于这个值的实体过滤掉能有效降低误报。4.2 Dockerfile 与 workflows/addr.yml 的部署意图项目里的Dockerfile大概率是基于paddlepaddle/paddle:latest或者python:3.8-slim构建的里面会装requirements.txt里的依赖把代码复制进镜像然后暴露一个端口或者设置一个入口命令。workflows/addr.yml看起来是 GitHub Actions 或者某种 CI 的工作流定义用来在代码 push 时自动跑训练或者部署。如果你只是本地用这两个文件可以先不动如果要上服务器做服务化Dockerfile里的CMD需要改成启动一个 HTTP 服务比如用 FastAPI 包一层usemodel.py的推理函数。# Dockerfile 典型结构基于 PaddlePaddle 官方镜像 FROM paddlepaddle/paddle:2.4.0-gpu-cuda11.2-cudnn8 WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple COPY . . # 启动推理服务假设用 FastAPI 暴露 8080 端口 CMD [python, server.py]requirements.txt里除了paddlenlp和paddlepaddle通常还会有fastapi、uvicorn、numpy这些。如果构建时 pip 下载慢可以在RUN pip install后面加-i指定国内源但注意不要写任何与网络代理相关的配置。4.3 推理性能的几个实测数字在单卡 V100 上UIE-base 对一条 200 字的中文文本做单实体类型抽取前向耗时大约 40-60ms。如果实体类型增加到 5 个因为要跑 5 次前向耗时会到 200-300ms。批量推理时可以把多条文本拼成一个 batch但要注意 padding 到同一长度会浪费算力建议按文本长度分桶后再组 batch。usemodel.py里如果用的是Taskflow它内部已经做了 batch 处理直接传 list 进去就行。5. 避坑与排查标注、训练、推理三个阶段最容易翻车的地方5.1 标注阶段Doccano 导出字段对不上现象doccano.py跑的时候报KeyError: label或者读出来的实体列表是空的。原因Doccano 不同版本导出的 JSONL 字段名不一样老版本用label新版本用entities而且start_offset和end_offset的键名也可能变成start和end。解决先拿一条导出数据print(json.loads(line).keys())看一眼实际字段然后在doccano.py里做兼容判断别硬编码字段名。5.2 训练阶段loss 不降或者直接变 NaN现象finetune.py跑了几十步 loss 一直在 0.69 附近晃或者突然跳到 NaN。原因loss 不降通常是learning_rate太小或者 prompt 描述词和预训练分布差太远NaN 多半是fp16在旧卡上的数值溢出或者max_seq_len设太大导致 attention 矩阵爆显存。解决先把learning_rate调到 5e-5 试一轮如果还不降就检查train.txt里的 prompt 拼接格式是不是少了分隔符。NaN 的话去掉--fp16把max_seq_len降到 256再不行就换uie-mini跑通流程再换回 base。5.3 推理阶段模型把「不是人名的词」也抽出来了现象usemodel.py对「张江高科技园区」抽出了「张江」作为人名。原因训练数据里「张江」这类词被标成了人名或者负样本里没有包含「地名人名歧义」的案例模型没学会区分。解决回 Doccano 把这类歧义样本补标进去同时在推理时设probability阈值比如只保留 0.8 以上的结果。如果业务对召回要求高可以保留低置信度结果但加一个人工复核环节。5.4 部署阶段Docker 容器里跑推理报缺少动态库现象本地python usemodel.py正常打进 Docker 后报libcudnn.so.8: cannot open shared object file。原因基础镜像的 CUDA 版本和宿主机驱动不匹配或者paddlepaddle-gpu装成了 CPU 版本。解决确认Dockerfile的FROM镜像里 CUDA 版本和宿主机nvidia-smi显示的驱动兼容requirements.txt里paddlepaddle-gpu的版本要和镜像里的 CUDA 对应。实在搞不定就先跑 CPU 推理UIE-base 在 CPU 上单条 200ms 左右小流量场景够用。5.5 数据切分train/dev/test 分布不一致导致 F1 虚高现象dev 上 F1 0.92换一批真实数据跑只有 0.6。原因doccano.py切分时是按文件顺序切的如果原始 JSONL 里前 80% 是简历文本、后 20% 是新闻文本train 和 dev 的领域分布就完全不同。解决切分前先对数据做 shuffle并且按实体类型分层采样保证每个集合里各类型实体的比例接近。sample_index.json这时候就有用了可以拿它做分层抽样的索引。6. 把 UIE 微调从「能跑」推到「能用」三个我反复验证过的技巧第一个技巧是 prompt 描述词的迭代。UIE 对 prompt 里的实体类型描述非常敏感用「人名」和用「人物姓名」在同一个数据集上 F1 能差 3-5 个点。我的做法是先用一个宽泛词跑一版 baseline然后拿 dev 集里预测错的样本看如果错在「该抽没抽」就把描述词改得更具体比如「人名」改成「中文姓名」如果错在「不该抽的抽了」就把描述词改得更严格比如「机构名」改成「公司名称」。这个迭代过程一般两三轮就能收敛。第二个技巧是负样本的构造。UIE 的 span 二分类损失里负样本就是那些「不是实体但和实体很像」的片段。我一般会从训练集里自动挖一批「被模型高置信度预测但不在标注里」的片段人工确认后作为负样本加进去。这批数据不用多500 条左右就能让 precision 提升 5 个点以上。具体做法是先用 baseline 模型跑一遍训练集把probability 0.5但不在 gold 标注里的 span 导出来人工过一遍确认是误报的就加到train.txt里prompt 部分留空表示「这段文本里没有该类型实体」。第三个技巧是推理时的 batch 分桶。usemodel.py如果直接对 list 里的文本逐条推理短文本和长文本混在一起会拖慢整体速度。我一般会先按文本长度排序然后每 8 条一组组内 padding 到该组最大长度这样比全局 padding 到 512 能快 2-3 倍。代码改动很小就是在extract_name外面包一层排序和分组逻辑。# 按长度分桶做批量推理减少 padding 浪费 def batch_extract(texts, ie, batch_size8): # 按文本长度排序并记录原始索引 indexed sorted(enumerate(texts), keylambda x: len(x[1])) results [None] * len(texts) for i in range(0, len(indexed), batch_size): batch indexed[i:ibatch_size] batch_texts [t for _, t in batch] batch_results ie(batch_texts) # Taskflow 支持 list 输入 for (orig_idx, _), res in zip(batch, batch_results): results[orig_idx] res return results这段代码的关键是sorted那一步把长度相近的文本排到一起组内 padding 的浪费就小了。batch_size设 8 是在 V100 上实测的甜点值再大显存收益递减再小则 batch 效应不明显。Taskflow的ie对象本身是线程安全的但如果你要起多进程做并发每个进程要单独初始化一个ie不能共享。从那以后我每次微调 UIE 之前都会先拿 50 条数据跑一遍完整流程——标注、转换、训练 1 个 epoch、推理——确认链路通了再上全量数据。这个习惯帮我省过至少两次「训了 8 小时发现标注格式错了」的后悔药。希望帮到你。本文还有配套的精品资源点击获取