任务定义
金融query 中的实体抽取
| ori_query | companies | institutions | industries | products |
| 众安在线最近海外营收占比多少 | 众安在线 | |||
| 哪些会议纪要提到欧洲央行利率决策 | 欧洲央行 | |||
| 越洋钻探最近营收增长多少 | 越洋钻探 | |||
| 过去六个月被下调评级的港股地产板块公司研报 | 地产板块 | |||
| 中国建筑国际2023年火锅底料销量是多少 | 中国建筑国际 | 火锅底料 |
而金融 query 实际上有几个非常特殊的优势:
- 句子短(通常 < 40 chars)
- 实体密度低
- 实体类型固定
- Query 语法高度模板化
- 中文金融实体具有强 lexical pattern
然后现在基本的NER 方案主要分成三类
| 维度 | Sequence Labeling (BERT+BIO) | GLiNER | UIE (生成式) |
|---|---|---|---|
| 架构 | Encoder + Token Classifier | Bidirectional Encoder + Span Matching | Seq2Seq / Generative LLM |
| 输出方式 | 每个token打标签 | Span + Type 相似度匹配 | 生成结构化文本(JSON等) |
| 实体类型灵活性 | 固定(Closed-set) | 开放(Zero-shot 强) | 开放(Zero-shot / Few-shot 最强) |
| 速度 | 最快 | 很快(推荐用于100ms场景) | 较慢 |
| 模型大小 | 中等 | 轻量(small/base 很小) | 较大(T5-11B 等) |
| 适用任务 | 主要NER(Flat) | 主要NER(可扩展多任务) | 通用IE(NER+RE+EE+...) |
| 训练/部署难度 | 低 | 中低 | 中高 |
| 当前地位 | 经典基线 | 2024-2026 实用零样本NER主流 | 复杂多任务场景主流 |
Sequence Labeling / Token Classification
Encoder + Token Classifier
工作方式:
- 对每一个 token独立预测标签(B-PER、I-ORG、O 等)。
- 属于token-level classification。
特点:
- 需要固定实体类型(closed-set),训练时标签是固定的ID。
- 速度快、结构简单、工业界早期主流。
- 零样本/开放实体能力很弱(新实体类型基本无法处理)。
- 标签本身没有语义(只是ID)。
标签建设
BIO 标注
BIO 的本质是序列标注(sequence labeling), 模型在做每个token 的分类
Begin(实体开始) Inside(实体内部) Outside(非实体)
e.g
query: 众安在线最近海外营收占比多少
entity: 众安在线(company)
| 字 | 标签 |
|---|---|
| 众 | B-COMPANY |
| 安 | I-COMPANY |
| 在 | I-COMPANY |
| 线 | I-COMPANY |
| 最 | O |
| 近 | O |
| 海 | O |
| 外 | O |
抽取
BERT→Linear→Softmax
pros&cons
pro
速度非常快
复杂度 O(seq_len)
con
1. 它是逐字决策,不理解“整体实体”
容易出现边界错误
2. bio 脆弱, 中间断一次,整个span 就断裂了
B-COMPANY
I-COMPANY
O
I-COMPANY
3. nested entity 很难
例如:
港股地产板块公司
里面:
港股地产板块(行业)
地产(更细粒度行业)
BIO 很难同时表示。
4. 长实体容易拖尾
例如:
中国建筑国际2023年火锅底料销量
模型可能:
中国建筑国际2023年
全打成 company。
BIOES
在实体抽取过程中,会存在“尾部多带一个字”的问题,这就是边界没训练好,在这种情况下,BIOES 通常比 BIO 更友好。
B = entity begin
I = entity inside
E = entity end
S = single-token entity
O = outside
如果是中文字符级标注,也可以用:
B / M / E / S / O效果类似。
e.g LabelSemantics(2022)
RoberTA + 对比学习
- 用一个BERT编码标签描述(如 “person”、“begin person” 等自然语言)。
- 用另一个BERT编码文本中的span / token。
- 通过相似度匹配(dot product + softmax)决定 token 属于哪个标签。
核心:对比学习标签表示和实体表示。
参考论文: https://aclanthology.org/2022.findings-acl.155/
base 模型: RoberTA
RoBERTa理解为是对 BERT 的一个“强化升级版”, 是训练更狠、更久、更规范的 BERT
- Encoder-only 模型(只有编码器,没有生成能力)
- 用来做:理解文本,而不是生成文本
具体改进包括:
- ❌ 去掉了 BERT 的 NSP(Next Sentence Prediction)任务
- 📈 用了更大的数据集
- ⏱️ 训练时间更长
- 📦 更大的 batch size
- 🎲 动态 masking(每次 mask 不一样)
这些改动让它在很多任务上都比 BERT 强。
🧩 能干啥?
典型 NLP 任务:
- 文本分类(情感分析、spam detection)
- NLI(自然语言推理)
- 问答(QA)
- 命名实体识别(NER)
👉 核心能力:理解一句话/一段话的语义
它不擅长生成:
- 写文章 ❌
- 对话 ❌
因为它不像 GPT 那样是 decoder-based。
可以这么想:
- GPT:会“说话”的人(生成)
- RoBERTa:擅长“读理解题”的人(理解)
标签空间 & 标签语义
双编码器 (LabelSemanticsNER):
token_encoder:编码输入句子,得到每个子词位置的 token_embeddings。
label_encoder:把每个 BIO 标签编成一段固定中文模板 + 描述的短文本,再编码;取 last_hidden_state[:, 0, :](序列第一个 token,一般是 [CLS])作为该标签的向量 label_representation。
分类方式
不是经典 Linear(hidden → num_labels),而是相似度:
对每个位置:logits = token_emb @ label_rep^T(实现里是 batch 维上的矩阵乘),argmax 得到预测标签 id。
标签向量缓存
评估时 use_label_cache=True,同一次 evaluate 里若已算过 label_representation 会复用,避免每个 batch 重算(reset_label_cache 在 evaluate 开头清空)。
训练
主损失:对 logits 与 labels 做 CrossEntropyLoss(ignore_index=-100),只在首子词等有效位置算。
对比损失(仅当 contrastive_weight > 0,且前向 return_representations=True):
在有效监督位上:labels != IGNORE_INDEX 且 english_mask 为真(即单字母英文 token 位置——设计上是让对比学习作用在你关心的英文场景子集)。
其中 实体类型为 contrastive_label(默认 C) 的 token 作为 anchor。
候选集 = 上述有效 token 的 embedding + 所有 label 的 representation;L2 归一化后算相似度 / temperature 得到 logits;去掉自身;正样本是「候选里类型为 C 的」。
形式是 log-sum-exp 的对比目标(与 supervised contrastive / InfoNCE 同类):拉近 anchor 与同类型(含对应标签向量),推远其他。
总损失:loss = ce_loss + contrastive_weight * contrastive
Generalist NER
https://github.com/fastino-ai/GLiNER2
核心定位:轻量级通用NER模型(Generalist & Lightweight)。
Bidirectional Encoder + Span Matching
工作方式:
- 使用双向Transformer Encoder(BERT-like)。
- 把实体类型名称(如 "person"、"organization")和文本一起输入模型。
- 通过span representation(跨度表示)+entity type embedding计算相似度(dot product + sigmoid),直接匹配哪些span属于哪个类型。
- 并行提取所有实体(非自回归)。
特点:
- Zero-shot / Open-set能力强:不需要为新实体类型重新训练,直接给实体类型列表就能抽。
- 仍然是encoder-only,非生成式,所以速度快(非常适合你之前说的100ms需求)。
- 比经典Sequence Labeling更灵活,但比LLM轻量得多。
- 主要专注NER(虽然后续有GLiNER2支持多任务IE)。
e.g GLiNER(2024)
- 把实体类型名称(如 “person”、“organization”)编码成 embedding。
- 把文本中的候选 span(连续的 token 组合)编码成 span representation。
- 通过dot product + sigmoid计算每个 span 和每个实体类型的匹配分数。
- 核心:span-entity type matching,本质也是对比/相似度匹配。
感觉这个工作和 LabelSematics 有点异曲同工之妙,都是基于语义匹配的 NER。GLiNER 可以看作是 Label Semantics 思路的进化版 + 工程优化版,在 zero-shot 能力和实用性上走得更远。
对比学习这块大差不差,我们看看encoder 这边的设计
Encoder 设计
Label Semantics encoder
- 架构类型:双编码器(Two separate BERT encoders),也叫Bi-encoder(但和后来GLiNER的bi-encoder不太一样)。
- 具体结构:
- Encoder 1(Document / Text Encoder):一个标准的BERT(或类似),负责编码输入文本。它输出每个token的表示(hidden states)。
- Encoder 2(Label Encoder):另一个独立的BERT,专门负责编码标签的自然语言描述。
- 例如:把 “PER” 转成 “person”、“begin person”、“inside person” 等自然语言句子。
- 取每个标签描述的[CLS]token 作为该标签的最终表示。
- 特点:
- 两个编码器完全独立,互不共享参数。
- 在推理时,标签表示可以预先计算并缓存(lookup table)。
- 后续通过token表示 vs 标签表示的相似度(dot product + softmax)来做匹配。
优势:标签语义利用得比较充分,适合 few-shot 场景。
劣势:两个独立BERT,参数量接近翻倍,计算效率相对较低。
GLiNER encoder
uniencoder
- 架构类型:单共享编码器(Shared Bidirectional Encoder),也叫Uni-encoder或Joint Encoder。
- 具体结构:
- 只有一个 bidirectional Transformer Encoder(通常是 DeBERTa-v3 或 BERT-like)。
- 输入构造:把文本和所有实体类型名称一起拼接成一个序列输入模型(例如:文本 + [SEP] + entity types list)。
- 这个同一个编码器同时负责:
- 生成文本中每个 token / span 的表示。
- 生成实体类型(label)的表示。
- 后续通过span representation(对连续 token 做 pooling 或特殊处理得到候选实体跨度表示)和entity type embedding计算相似度(dot product + sigmoid)。
特点:
- 参数高效:只有一个编码器,模型整体更轻量。
- 交互更充分:文本和标签在同一个编码器中一起处理,能捕捉更直接的上下文交互。
- 推理时所有东西一起算,没有完全独立的 label encoder。
- 每条query进来,模型都要重新编码一次所有实体类型。
- 当实体类型数量增多(比如50+,甚至几百个)时,输入序列变长,计算量显著增加(尤其是attention计算是平方级的)。
- 结果:实体类型越多,速度下降越明显(论文中提到100+个标签时性能严重退化)。
bi-encoder(解耦版)
新工作又给它把文本和标签解耦, 效率反而更高了
为什么解耦后效率更高?
把重复计算的部分彻底隔离并缓存
- 两个独立编码器:
- Text Encoder:只编码输入文本(query)。用 ModernBERT / Ettin系列(专门优化过的长上下文encoder),负责处理query和生成span表示。
- Label Encoder:只编码实体类型名称(label descriptions)。用预训练的Sentence Transformer(如 BGE、MiniLM、all-MiniLM等),这些模型天生就擅长编码短文本的语义,已经在海量数据上做过句子级对比学习。
两者通过训练时的对比损失(contrastive loss)强制对齐到同一个语义空间,而不是靠同一个模型来“天然相似”。
- 关键优化:实体类型(Label)表示可以提前预计算并缓存。
- 如果你的实体类型列表是固定的(或很少变化),Label embeddings 可以只算一次,存成向量库。
- 每次推理时,只需要跑Text Encoder,然后直接做向量点积匹配(非常快)。
Q: 为啥非要双编码器,不能同一个编码器编码label 之后存起来,另外一个编码query, 这样在语义空间上还更相似呢
文本和标签的特性差异很大:
- Label(实体类型):通常是非常短的文本(1-5个词,比如“person”、“上市子公司”、“发货地址”)。
- Query/Text:是完整句子,有丰富上下文,需要捕捉span(跨度)的语义。
如果强行用同一个模型同时处理这两种完全不同长度的输入,模型容易顾此失彼(trade-off),导致整体表示质量下降。
干扰问题:
- 同一个编码器在训练时,注意力机制会同时看到长文本和短标签,容易让模型对短标签的编码不够“专业”。
- 实际实验显示,共享权重在大规模实体类型(几百上千个)时表现变差。
即使你用同一个模型编码label存起来,语义空间的“相似性”也不是天然最好的。通过专门模型 + 针对性对比训练,反而能把两种不同性质的文本更好地对齐到同一个向量空间里。
凭什么Gliner 泛化性更好?
- 从“固定分类”变成“开放匹配”(Matching vs Classification)
- 传统 BERT + BIO:标签是固定 ID(closed-set),模型学的是“这个 token 属于第几个类别”。遇到训练时没见过的实体类型,几乎完全无法处理(需要重新训练分类头)。
- GLiNER:把 NER 变成span(文本跨度)与实体类型描述(自然语言)之间的相似度匹配。 实体类型不再是 ID,而是“person”、“上市子公司”、“发货地址”等文本描述,模型在共享的语义空间中计算匹配分数。 → 这让它天然支持open-set / zero-shot:只要给新标签的文字描述,就能尝试识别,即使训练时完全没见过。
- 标签语义被充分利用GLiNER(尤其是 bi-encoder 版本)用专门的 Label Encoder(Sentence Transformer 类)编码标签描述,Text Encoder 编码文本 span。两者通过对比学习(contrastive loss)对齐到同一个向量空间。 这让模型对未见实体类型的理解能力远强于传统模型(传统模型标签语义几乎为0)。
- 训练数据和目标的差异
- GLiNER 在训练时用了Pile-NER等大规模、多样化的数据集,覆盖了海量不同实体类型。
- 目标是“通用匹配”,而不是“在固定几个类别上分类”,所以模型被迫学到更 robust 的表示。
- 实验结果显示:即使是 small 版本(几十M参数),在out-of-domain(跨领域)zero-shot 测试上,也能超过 ChatGPT、InstructUIE 等大模型。
- Span-level 而非纯 Token-level它直接对可能的实体跨度(span)做表示 + 匹配,而不是逐 token 打 BIO 标签,更容易捕捉完整实体,尤其在边界模糊或复杂场景下泛化更好。
pros & cons
pro:
- 在多个 zero-shot 跨领域基准(如 CrossNER)上,GLiNER bi-encoder 大版本达到了61.5% Micro-F1,是当时 SOTA 水平。
- 即使小模型,也经常在零样本设置下打败参数量大几十倍的 LLM。
- 在生物医学、法律、多语言等垂直/低资源场景,泛化优势特别明显,因为它不需要为每个新领域重新收集大量标注数据。
con:
在特定领域有大量高质量标注数据时,传统 fine-tuned BERT-CRF 可能在精度上还略胜(因为它“死记硬背”得更准)。
长尾复杂实体、上下文极强的消歧上,仍然可能需要 LLM 辅助。
UIE (Universal Information Extraction)
Seq2Seq / Generative LLM
论文 Unified Structure Generation for Universal Information Extraction
它不是传统 BIO NER,而是一种更像“给模型一个抽取指令/schema,让它按这个 schema 从文本里找 span”的方案。
工作方式:
- 生成式(Generative):基于T5、Flan-T5等seq2seq模型。
- 用自然语言prompt + schema(模式),让模型生成结构化输出(JSON、线性化结构、SEL等)。
- 一个模型统一处理 NER、关系抽取(RE)、事件抽取(EE)、情感分析等。
特点:
- 最灵活:支持复杂结构化输出、层次化信息、多任务。
- Zero-shot / Few-shot能力很强。
- 缺点:自回归生成,速度慢(尤其是长输出时),模型通常较大。
- 更适合复杂IE场景,而不只是简单实体提取。
输入文本: 众安在线最近海外营收占比多少 抽取 schema: 公司、机构、行业、产品 模型输出: 公司:众安在线普通 NER 是固定标签训练:
众 B-company 安 I-company 在 I-company 线 I-companyUIE 更像是问模型:
请从这句话中抽取“公司” 文本:众安在线最近海外营收占比多少然后模型预测 span:
众安在线InstructUIE
基于LLM的指令微调版本。
后续很多LLM-based工作都沿这个路线(schema prompt + 生成结构化输出)。