3步跑通LayoutLMv3表单实体抽取
【免费下载链接】Transformers-TutorialsThis repository contains demos I made with the Transformers library by HuggingFace.项目地址: https://gitcode.com/GitHub_Trending/tr/Transformers-Tutorials
扫描件上的申请表、发票、病历,想批量抠出姓名、金额、日期,正则写到一半就劝退了。LayoutLMv3 把 OCR 文字、边界框和整页图像一起喂给模型,专治表单实体识别这类文档理解问题,直接输出每个词的实体标签。跟着官方 notebook 走,1000 步就能把 F1 拉到 90% 附近,数据格式、微调参数、推理可视化一次跑通。
为什么表单实体识别需要版面信息
纯文本模型看一份表单,只会看到一堆词的序列:「姓名 张三 日期 2024-01-05」。但「张三」属于哪个字段,靠的是它紧挨着「姓名」这个标签、在同一行、坐标挨得近——这些信息全在版面里。LayoutLMv3 的做法是把三路输入融合进同一个 Transformer:词本身(tokens)、每个词的二维坐标(bbox)、整页文档图像(image),预测时就同时考虑"写了什么"和"写在哪里"。
它和上一代 LayoutLMv2 的训练流程几乎一样,差异集中在两处:分词从 BERT 式 WordPiece 换成了 RoBERTa 的字节级 BPE;图像不再由模型内部做 BGR 处理,而是要求你预先 resize、归一化成 RGB 通道的pixel_values(3, 224, 224)。真正拉开差距的是位置编码粒度——v3 用 segment 级位置嵌入,属于同一段落/字段的词共享同一组 bbox 坐标,从而获得相同的 2D 位置编码,这也是它在 FUNSD 上能稳过 90% F1 的关键。
相关代码入口:LayoutLMv3/,完整流程就写在一个 notebook 里:Fine_tune_LayoutLMv3_on_FUNSD_(HuggingFace_Trainer).ipynb.ipynb)。
核心实操:FUNSD数据集标注格式与1000步微调
⚡️ 下面三步就是官方 notebook 的完整路径:备数据 → 跑微调 → 可视化验证。数据集用nielsr/funsd-layoutlmv3,是表单理解的标准评测集,每条样本四个字段:tokens(词列表)、bboxes(坐标)、ner_tags(标签)、image(原图),标注格式和你的业务字段几乎可以一比一对应。
加载数据集并用Processor对齐图文
目标是把每条样本变成模型能吃的张量。LayoutLMv3Processor内部封装了图像处理器和分词器,一次调用同时产出pixel_values、input_ids、bbox、labels,不用自己拼。这里有个小细节:数据集已经带 OCR 结果,加载时要传apply_ocr=False,省得 processor 再跑一遍 OCR。
下面的代码把 processor 包装成 map 函数,批量处理整个训练集:
from transformers import AutoProcessor from datasets import load_dataset dataset = load_dataset("nielsr/funsd-layoutlmv3") processor = AutoProcessor.from_pretrained("microsoft/layoutlmv3-base", apply_ocr=False) def prepare_examples(examples): return processor(examples["image"], examples["tokens"], boxes=examples["bboxes"], word_labels=examples["ner_tags"], truncation=True, padding="max_length") train_dataset = dataset["train"].map(prepare_examples, batched=True, remove_columns=dataset["train"].column_names)注意 map 时官方还会显式声明features,其中pixel_values是 (3, 224, 224) 的 Array3D、bbox是 (512, 4) 的 Array2D,这样set_format("torch")之后张量形状才稳定。
✅ 验证方式:对第一条样本跑processor.tokenizer.decode(example["input_ids"])应能还原出可读文本;打印各字段 shape,pixel_values为 (3, 224, 224)、bbox为 (512, 4) 就说明对齐没问题。
配置Trainer参数跑完1000步
目标是在预训练权重上把 token 分类头调出来。模型用LayoutLMv3ForTokenClassification,加载时传入id2label/label2id(从数据集的ClassLabel生成),指标用 seqeval 算实体级 P/R/F1。超参直接照官方 notebook 的配置走,各参数的取舍如下:
| 参数 | 推荐值 | 设置原因 |
|---|---|---|
| per_device_train_batch_size | 2 | 224×224 图像加 512 长度序列,显存吃紧时先压到 2 |
| learning_rate | 1e-5 | 官方默认值,配合 1000 步能稳定收敛 |
| max_steps | 1000 | FUNSD 训练集只有几百张,1000 步足够,比按 epoch 跑更好控制 |
| eval_steps | 100 | 每 100 步算一次 F1,曲线看得清楚 |
| metric_for_best_model | f1 | 配合load_best_model_at_end自动保留最佳 checkpoint |
模型、参数、Trainer 三件套合起来就这几行:
model = LayoutLMv3ForTokenClassification.from_pretrained( "microsoft/layoutlmv3-base", id2label=id2label, label2id=label2id) training_args = TrainingArguments( output_dir="test", max_steps=1000, per_device_train_batch_size=2, learning_rate=1e-5, evaluation_strategy="steps", eval_steps=100, load_best_model_at_end=True, metric_for_best_model="f1") trainer = Trainer(model=model, args=training_args, train_dataset=train_dataset, eval_dataset=eval_dataset, tokenizer=processor, data_collator=default_data_collator, compute_metrics=compute_metrics) trainer.train()✅ 验证方式:训练日志第 1000 步的 F1 应落在 0.85~0.90 区间(官方一次典型运行是 0.899),output_dir下出现checkpoint-1000目录就说明跑通了。
加载checkpoint推理并把结果画回原图
目标是肉眼确认模型抽的字段对不对。推理时没有真值标签,labels只在词的首个子词位置有效、其余是 -100,所以比对和画图都要先过滤掉这些位置;bbox 是 0~1000 的归一化坐标,画回图上前要按图像宽高反归一化:
model = AutoModelForTokenClassification.from_pretrained("test/checkpoint-1000") with torch.no_grad(): outputs = model(**processor(image, words, boxes=boxes, return_tensors="pt")) predictions = outputs.logits.argmax(-1).squeeze().tolist() def unnormalize_box(bbox, w, h): return [w * bbox[0] / 1000, h * bbox[1] / 1000, w * bbox[2] / 1000, h * bbox[3] / 1000] for pred, label, box in zip(predictions, labels, encoding.bbox.squeeze().tolist()): if label != -100: # 只在词的首个子词位置比对 draw.rectangle(unnormalize_box(box, *image.size), outline=label2color[label])✅ 验证方式:原图上按类别着色的框(question 蓝、answer 绿、header 橙)应和官方 notebook 里贴的 ground truth 基本重合。
📌 再往前一步:真上线时你拿不到word_labels,得用 tokenizer 返回的offset_mapping把子词预测聚合回词级实体,仓库里有个现成的配套示例:True_inference_with_LayoutLMv2ForTokenClassification_+_Gradio_demo.ipynb,做法对 LayoutLMv3 完全等价。
踩坑与调优:LayoutLMv3微调最容易卡住的5个点
🔍 以下每条都是"现象 → 原因 → 解法",按踩中概率排序:
- 图像预处理照搬v2旧代码,loss 降得特别慢。v2 时代的预处理是 BGR 通道且由模型内部归一化,而 v3 换掉视觉骨干后期望 RGB 的 (batch, 3, 224, 224)
pixel_values,旧写法等于喂了张色偏图。解法是图像侧全部交给LayoutLMv3Processor处理,别手写 transform。 - F1 比预期低一大截。对比预测和标签时没过滤 -100,特殊 token 和词内部子词位置全被算进 seqeval,召回率被拖低。原因是 BPE 分词下只有词的首个子词带真实标签,其余位置填 -100。解法是算指标和比对预测时统一用
label != -100过滤。 - 线上推理时不知道哪个 token 是词首。训练时有
word_labels可以对位,新扫描件没有标签,argmax 出来的一串子词级预测拼不成实体。原因是子词到词的对齐信息不在模型输出里。解法是取 tokenizer 的offset_mapping把子词映射回原词再聚合。 - bbox 直接画到图上,框全挤在左上角。
encoding.bbox里的坐标是 0~1000 的归一化值,直接传给 PIL 会当成像素。解法是按图像宽高除以 1000 反归一化之后再画。 - F1 卡在 80% 出头,怎么调参都不动。OCR 输出里每个词各带一个独立 bbox,词级位置编码让模型丢掉了"哪些词属于同一段"的信号。原因是 v3 的核心收益来自 segment 级位置编码——同一字段的词共享坐标才有相同位置嵌入(LayoutLMv3/README.md 里专门强调了这点)。解法是标注时按字段/行把词归成 segment,Tesseract 等引擎本身就能输出 segment 结构,别拆散。
选型与下一步:什么文档适合LayoutLMv3
它适合有 OCR 文本层加坐标的结构化文档——表单、发票、申请单、病历页,字段固定、版面重复的场景收益最大;没有文本层的纯扫描件要先生成 OCR 文本,手写体和艺术字也不在它的舒适区,这类需求仓库里可以看 UDOP、MarkupLM 等文档理解方向做对比。同门的 LayoutLMv2/FUNSD/ 还有配套的真推理 Gradio demo,想搭个在线试用的界面可以直接参考。下一步就一个动作:git clone https://gitcode.com/GitHub_Trending/tr/Transformers-Tutorials,打开 LayoutLMv3 的 FUNSD notebook.ipynb) 逐格跑通,再把数据集换成你自己的标注。
【免费下载链接】Transformers-TutorialsThis repository contains demos I made with the Transformers library by HuggingFace.项目地址: https://gitcode.com/GitHub_Trending/tr/Transformers-Tutorials
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考