news 2026/11/2 11:49:16

BERT-NER微调实战:BIO标签、分词对齐与F1排查

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
BERT-NER微调实战:BIO标签、分词对齐与F1排查

简介:面向自然语言处理与BERT入门进阶学习者的实操型PDF资料,聚焦中文命名实体识别(NER)的微调落地:从地址、公司、姓名、职位等10类实体与B-/I-标签编码,到BertTokenizer中文分词、input_ids/token_type_ids/attention_mask解析,再到分词后标签对齐、模型微调、准确率/召回率/F1评估,帮助读者理解并复现BERT-NER流程。包内仅1个PDF文件,约545KB,内容紧凑,适合碎片时间系统阅读,也便于对照代码片段梳理实现思路。已有1245人学习下载,可作为课程实验、项目预研或NER小任务的基础参考资料,重点覆盖实体定义、标签映射与数据预处理等易错环节。

1. 从一句中文到 21 个标签:这份 BERT-NER 微调文档到底能跑出什么

有人把 BERT 预训练模型微调实现命名实体识别任务跑成文本分类:训练 loss 一路下降,验证集 F1 却贴着 0。翻车点通常不在学习率,而在标签字典、分词对齐和样本构造这三处。这份文档把 NER 拆成一条能照抄的链路:先定义 address、book、company 等 10 类实体并生成 21 个 B/I/O 标签,用 BertTokenizer 把中文句子转成 input_ids、token_type_ids、attention_mask,再写 get_token_label 把字符级 BIO 对齐到 subword 级 token,最后构造样本、接分类头、训练和评估。它适合已经会跑通 transformers 基础示例、但一换自己的标注数据就卡在标签错位上的从业者;新手按第 2 到第 4 章的代码顺序走,也能把一条中文 NER 微调流水线搭起来。

2. 标签体系与 BIO 编码:先把 21 类实体字典钉死

命名实体识别不是先加载模型,而是先确定实体边界。BERT 微调只是把 token 映射到标签;标签体系错了,模型越训越自信地错。10 个实体类,BIO 编码会生成 21 个标签:1 个 O,10 个 B-,10 个 I-。O 必须放在 0 号位,因为大多数分类头和交叉熵都默认 0 是背景类;B-company 和 I-company 的顺序一旦定下,训练、评估、推理、保存加载都要认同一套映射。

2.1 为什么 NER 的第一行代码不是加载模型,而是定义实体边界

文本分类的 label 是整句一个,NER 的 label 是每个 token 一个,所以 NER 的标签表本身就是数据规范的一部分。address、book、company、game、government、movie、name、organization、position、scene 这 10 类实体,来自标注规范里的实体边界定义。B- 表示实体开始,I- 表示实体内部,O 表示非实体。没有 B- 和 I- 的区分,模型只能学到“这个字属于公司”,学不到“公司名从哪个字开始、到哪个字结束”。

我一般会先让标注同学确认三件事:实体是否允许嵌套、是否允许重叠、英文和数字是否算实体内部。如果允许嵌套,BIO 就不够用,需要换 BIEOS 或分层标注;如果业务只关心实体片段,BIO 够用,但后处理要按 B- 开新实体、I- 延续实体、O 关闭实体。这里最怕的是标注文件里 B-company 后面直接跟 O,模型会学到“公司名只有一个字”的错误边界。

还有一个常被忽略的点:实体类别的顺序会影响 id。你这次把 address 放第 1 个,下次有人把 book 插到前面,id2label 全变,旧模型加载后就会把公司识别成书籍。标签字典不是临时变量,它是模型资产。

2.2 从 entities 到 id2label:21 个标签的生成与校验

下面这段代码是整条链路的起点。它不依赖模型,只依赖实体列表和 BIO 规则,先把 21 个标签和数字的对应关系生成出来,再做断言校验。

# 实体类型来自业务标注规范,顺序一旦定下就不要随意改 entities = [ 'address', 'book', 'company', 'game', 'government', 'movie', 'name', 'organization', 'position', 'scene' ] # O 表示非实体;B- 表示实体开始;I- 表示实体内部 label_names = ['O'] + ['B-' + x for x in entities] + ['I-' + x for x in entities] id2label = {i: label for i, label in enumerate(label_names)} label2id = {v: k for k, v in id2label.items()} assert len(label_names) == 21 assert label2id['O'] == 0 assert id2label[label2id['B-company']] == 'B-company' print(id2label) print(label2id)

entities 的顺序决定 B- 区间和 I- 区间的 id:0 是 O,1 到 10 是 B-address 到 B-scene,11 到 20 是 I-address 到 I-scene。label_names 用列表推导生成,保证 B- 和 I- 的数量与实体数一致。id2label 用 enumerate 生成,label2id 用反转字典生成,两个方向必须能互相还原。三个 assert 是后悔药:只要标签数不是 21、O 不在 0、B-company 不能回读,训练前就会报错,不用等 loss 变成 NaN 才查。

参数上要注意:实体名不要带空格和特殊符号,B- 和 I- 前缀用英文大写,实体名统一小写。如果标注文件里出现 B-Apparel、I-Apparel,而代码里是 B-address,映射时会直接 KeyError。新增实体时不要手工往 id2label 里插键,应该改 entities 后重新生成整套映射,并把新的 label2id 写进模型配置。

2.3 用一张表核对 B/I/O 的对应关系

标签表生成后,别急着喂数据。先拿一张表把区间、含义和数量对一遍,尤其是标注文件里出现的标签是否都在表里。

ID 范围标签形态数量含义
0O1非实体 token
1 到 10B-address 到 B-scene10实体开始 token
11 到 20I-address 到 I-scene10实体内部 token

这张表的价值在于排查“标签集不匹配”。如果 char_label.txt 里出现 B-Apparel,而 label2id 里只有 B-address 到 B-scene,make_sample 里的label2id[x]会直接抛异常。另一种更隐蔽的情况是标注文件里只有 O 和 B-,没有 I-,模型学不到多字实体,验证集 F1 会卡在很低的水平。用集合比对能提前发现:

# 假设 all_labels 是从 char_label.txt 收集到的所有标签 all_labels = {'O', 'B-company', 'I-company', 'B-name', 'I-name'} missing = all_labels - set(label2id.keys()) extra = set(label2id.keys()) - all_labels print('标注里有但字典里没有:', missing) print('字典里有但标注里没有:', extra)

这段检查不涉及模型,只是在读数据阶段做集合差。missing 非空说明标签名拼错或实体类型没登记;extra 非空不一定是错误,小样本可能没覆盖全部实体,但如果训练集很大仍然大量 extra,就要检查标注规范是否只用了部分类别。参数上,all_labels 应该从训练集和验证集分别收集,验证集出现训练集没有的标签时,评估结果没有意义。

2.4 把 label2id 写进配置,避免训练/推理各说各话

标签映射生成后,最稳妥的做法是跟模型一起保存。训练时用BertForTokenClassification.from_pretrained(..., num_labels=21, id2label=id2label, label2id=label2id),保存时用save_pretrained,这样 config.json 里会带 id2label 和 label2id。推理脚本加载模型后先读model.config.id2label,不要自己在推理文件里重新手写一份字典。

import json with open('label2id.json', 'w', encoding='utf-8') as f: json.dump(label2id, f, ensure_ascii=False, indent=2) with open('label2id.json', 'r', encoding='utf-8') as f: restored = json.load(f) assert restored == label2id

这段代码做的是落盘和回读校验。ensure_ascii=False让中文实体名可读,indent=2方便 diff。回读后断言相等,能防止手改 JSON 后 key 顺序或 value 类型变化。常见坑是把 label2id 存成字符串,加载后 key 变成字符串、value 也变成字符串,模型输出 logits 的 argmax 是整数,映射时对不上。训练前跑一次回读,比上线后查黑匣子便宜。

3. 分词与标签对齐:subword、[UNK]、特殊 token 怎么不把标注冲散

BERT 的 tokenizer 不是按“字”切到底。中文大多按单字切,英文单词可能拆成 subword,生僻字可能变成 [UNK],句子首尾还会加 [CLS] 和 [SEP]。原始标注是字符级 BIO,每个字符一个标签;tokenizer 输出是 token 级序列,长度经常不等于原文本长度。标签对齐就是在这两个序列之间做逐 token 映射,映射错了,模型看到的监督信号就是错位的。

3.1 BertTokenizer 的输出到底有哪些字段

先看一个最小例子。输入一句中文,tokenizer 返回三个核心字段:input_ids、token_type_ids、attention_mask。单句任务里 token_type_ids 全是 0,attention_mask 有效 token 为 1、padding 为 0。

from transformers import BertTokenizer model_name = 'bert-base-chinese' tokenizer = BertTokenizer.from_pretrained(model_name) text = '某年某遗址出土的织物残片,图中纹样清晰。' encoded = tokenizer(text) print(encoded['input_ids']) print(encoded.keys()) tokens = tokenizer.convert_ids_to_tokens(encoded['input_ids']) print(tokens)

from_pretrained加载中文词表,tokenizer(text)默认加特殊 token,不做 padding,也不截断。input_ids 是词表 id 序列,token_type_ids 区分句子对,attention_mask 标记哪些位置参与注意力。convert_ids_to_tokens 把 id 还原成 token 字符串,方便肉眼检查。中文标点、数字、英文混排时,token 可能变成[UNK],也可能被拆成多个 subword,长度和原文本不一致。

字段作用单句任务取值常见误用
input_idstoken 的词表 id[101, ..., 102]直接当标签用
token_type_ids区分句子对全 0多句任务忘记改
attention_mask标记有效 token有效为 1,padding 为 0padding 后不传给模型

提示:中文模型里 101 通常是 [CLS],102 是 [SEP],100 是 [UNK]。不同词表可能不同,排查时用tokenizer.convert_ids_to_tokens打印,不要硬编码 id。

3.2 字符级 BIO 与 token 级 label 的长度差从哪来

长度差主要有四个来源。第一,[CLS] 和 [SEP] 是额外 token,没有对应字符标签。第二,英文单词可能被拆成play、##ing这样的 subword,一个词对应多个 token。第三,生僻字或特殊符号可能变成 [UNK],一个 token 对应一个或多个字符。第四,padding 和截断会改变序列长度,如果 labels 不同步截断或补 -100,loss 会把 padding 当成实体来学。

对齐策略要按 token 类型分开处理:单字符 token 直接取当前字符标签;特殊 token 给 O;[UNK] 继承当前字符标签;subword 去掉##后逐字符推进,标签取该 subword 覆盖字符的起始标签。多字 token 如果整体是 B-company,就保持 B-company;如果内部是 I-company,就保持 I-company。关键是每个 token 都要有标签,且 token_labels 长度必须等于 tokens 长度。

3.3 get_token_label 的逐 token 对齐逻辑拆解

下面这个函数是文档里的核心对齐逻辑。它同时推进三个迭代器:token、字符、字符标签。每处理一个 token,就把字符指针推到对应位置,保证下一个 token 从正确的字符开始。

def get_token_label(text, char_label, tokenizer): tokenized_input = tokenizer(text) tokens = tokenizer.convert_ids_to_tokens(tokenized_input['input_ids']) iter_tokens = iter(tokens) iter_char_label = iter(char_label) iter_text = iter(text.lower()) token_labels = [] t = next(iter_tokens) char = next(iter_text) char_tp = next(iter_char_label) while True: # 单字符 token,如大多数汉字,直接对齐 if len(t) == 1: assert t == char, f'token/char 不一致: {t} vs {char}' token_labels.append(char_tp) try: char = next(iter_text) char_tp = next(iter_char_label) except StopIteration: pass # 特殊 token 给 O,但 [UNK] 单独处理 elif t in tokenizer.special_tokens_map.values() and t != '[UNK]': token_labels.append('O') # [UNK] 继承当前字符标签,并继续推进字符 elif t == '[UNK]': token_labels.append(char_tp) # 多字符 token,如英文 subword else: t_label = char_tp t_clean = t.replace('##', '') for c in t_clean: assert c == char or char not in tokenizer.vocab if t_label != 'O': t_label = char_tp try: char = next(iter_text) char_tp = next(iter_char_label) except StopIteration: pass token_labels.append(t_label) try: t = next(iter_tokens) except StopIteration: break # 非特殊 token 要先把字符指针推到 token 首字符 if t not in tokenizer.special_tokens_map.values(): while char != t[0]: try: char = next(iter_text) char_tp = next(iter_char_label) except StopIteration: pass continue assert len(token_labels) == len(tokens), (len(token_labels), len(tokens)) return token_labels

参数上,text 是原句,char_label 是字符级标签列表,长度应等于 text 的字符数,tokenizer 是已加载的 BertTokenizer。函数内部用text.lower()是为了和 uncased 词表行为一致;如果 tokenizer 区分大小写,就不要 lower。特殊 token 判断用tokenizer.special_tokens_map.values(),[UNK] 被排除后单独处理。subword 去掉##后逐字符断言,防止字符指针漂移。最后的 assert 是底线:长度不等就直接报错,不要带着错位标签继续训练。

3.4 用一个小样本验证对齐,不要等 loss 降不下去才回头查

对齐函数写完,先构造一个短句和字符标签,打印 token 和 label 的对应关系。下面用虚构的“甲在乙公司工作”做样例,标签是字符级 BIO。

text = '甲在乙公司工作' char_label = ['B-name', 'O', 'B-company', 'I-company', 'I-company', 'O', 'O'] tokens = tokenizer.convert_ids_to_tokens(tokenizer(text)['input_ids']) aligned = get_token_label(text, char_label, tokenizer) for t, l in zip(tokens, aligned): print(repr(t), l)

预期输出里,[CLS] 和 [SEP] 是 O,甲是 B-name,乙是 B-company,公、司是 I-company,在、工、作是 O。如果甲变成 O,说明特殊 token 分支把字符指针推错了;如果乙公司三个字只有一个标签,说明多字符 token 分支没有逐个 append。这个样例跑通,再换英文、数字、生僻字、中文标点各造一条,覆盖 tokenizer 的边界行为。

注意:不要用tokenizer.encode代替tokenizer(text)做对齐。encode 会丢掉 attention_mask 等信息,后续拼样本时还要再补,容易出现 labels 和 input_ids 长度不一致。

4. 样本构造与训练入口:从 text/label 两个 txt 到 Dataset

对齐函数只是工具,真正喂给模型的是样本。text.txt 存原句,char_label.txt 存字符级标签,两个文件用空行分句。读取时要保证句子块数一致、每块字符数一致,再做 tokenizer 编码和标签映射。训练入口则要接 BertForTokenClassification,输出维度等于标签数,评估用实体级 F1。

4.1 读取 BIO 文本:空行切句与文件尾处理

下面两个函数分别读文本块和标签块。文本文件每行可能是一个字符,也可能是分词后的词,''.join(buf)把它们拼回原句;标签文件每行一个标签,直接保留列表。

def read_text_blocks(path): blocks, buf = [], [] with open(path, 'r', encoding='utf-8') as f: for raw in f: line = raw.strip() if line: buf.append(line) elif buf: blocks.append(''.join(buf)) buf = [] if buf: blocks.append(''.join(buf)) return blocks def read_label_blocks(path): blocks, buf = [], [] with open(path, 'r', encoding='utf-8') as f: for raw in f: line = raw.strip() if line: buf.append(line) elif buf: blocks.append(buf) buf = [] if buf: blocks.append(buf) return blocks

encoding='utf-8'必须显式指定,Windows 默认编码可能把中文读坏。strip()去掉换行和首尾空格;如果实体内部有空格,不能 strip 标签行,要按数据规范处理。空行是句子分隔符,文件末尾没有空行时,最后的 buf 要手动 flush,否则最后一句会丢。参数上,text 文件如果每行是整句而不是单字,就不要用''.join,改成按行收集整句;这份文档的标注格式是字符级,所以按字符拼接。

读完后先做一致性检查,不要直接进 tokenizer:

texts = read_text_blocks('text.txt') char_labels = read_label_blocks('char_label.txt') assert len(texts) == len(char_labels), '句子数不一致' for i, (t, ls) in enumerate(zip(texts, char_labels)): assert len(t) == len(ls), f'第 {i} 句长度不一致: {len(t)} vs {len(ls)}'

这段检查能挡住最常见的标注错位:文本 20 个字,标签 21 个,后面所有样本都会错。句子数不一致通常是一个文件末尾多空行,或者某个句子中间有空行被提前切开。

4.2 make_sample:把 tokenizer 输出拼成模型可吃的样本

make_sample 做三件事:tokenizer 编码、标签对齐、标签转 id。这里补上截断和 padding,避免长句把显存打爆。

def make_sample(text, char_label, tokenizer, label2id, max_len=128): encoded = tokenizer( text, truncation=True, max_length=max_len, padding='max_length', return_tensors=None ) token_labels = get_token_label(text, char_label, tokenizer) token_labels = token_labels[:max_len] labels = [label2id[x] for x in token_labels] labels = labels + [-100] * (max_len - len(labels)) sample = dict(encoded) sample['labels'] = labels return sample

truncation=True和max_length=128控制输入长度;padding='max_length'让 batch 内长度一致;return_tensors=None返回 Python list,方便后面转 Dataset。token_labels 先截断到 max_len,再映射 id。padding 位置用 -100 填,因为交叉熵默认忽略 -100,不会把 padding 当 O 来学。如果 padding 填 0,模型会把大量 padding 学成 O,验证集 F1 看起来不差,但实体边界会变钝。

参数上,max_len 建议从 128 起,实体跨度大的场景用 256;再大先看显存和句长分布。label2id 必须和训练配置一致,不能这里用一套、模型 config 里用另一套。token_labels 截断后如果长度小于 max_len,补 -100;如果等于 max_len,不补。

4.3 训练参数与模型头:batch size、学习率、max_len 怎么定

模型头用 BertForTokenClassification,num_labels 等于标签数,id2label 和 label2id 一起传进去。学习率不要用文本分类那套 1e-3,BERT 微调常见 2e-5 到 5e-5,batch size 8 或 16 起步。

from transformers import BertForTokenClassification model = BertForTokenClassification.from_pretrained( 'bert-base-chinese', num_labels=len(label_names), id2label=id2label, label2id=label2id ) training_args = { 'learning_rate': 3e-5, 'per_device_train_batch_size': 8, 'per_device_eval_batch_size': 8, 'num_train_epochs': 3, 'weight_decay': 0.01, 'logging_steps': 50, 'eval_steps': 200, 'save_steps': 200, 'load_best_model_at_end': True, 'metric_for_best_model': 'f1' }

num_labels=21决定分类头输出维度,写错会在加载权重时报 shape mismatch。learning_rate=3e-5是中文 NER 的稳妥起点;数据量小可以降到 2e-5,数据量大可以到 5e-5。batch size 8 对 128 长度通常够用,OOM 就减到 4 并开梯度累积。epoch 3 到 5 先跑,验证集 F1 连续不升就停。weight_decay=0.01抑制过拟合,load_best_model_at_end配合 f1 指标保留最优 checkpoint。

4.4 评估用 seqeval,不要用 accuracy 糊弄自己

NER 的标签大部分是 O,accuracy 可以很高但实体一个没识别对。评估要用实体级 precision、recall、F1,seqeval 的 classification_report 能按实体类型给出结果。

from seqeval.metrics import classification_report, f1_score def decode_predictions(pred_ids, label_ids, id2label): pred_labels, true_labels = [], [] for p, l in zip(pred_ids, label_ids): if l == -100: continue pred_labels.append(id2label[p]) true_labels.append(id2label[l]) return pred_labels, true_labels # pred_ids 和 label_ids 是单句的 list,按需在评估循环里收集 pred_labels, true_labels = decode_predictions(pred_ids, label_ids, id2label) print(classification_report([true_labels], [pred_labels])) print('entity f1:', f1_score([true_labels], [pred_labels]))

decode_predictions跳过 -100 的 padding,避免把填充位置算进指标。seqeval 接收的是“句子列表的标签列表”,所以外面再包一层列表。classification_report能看出哪类实体 recall 低,比如 company 的 B/I 边界经常错,就回去查对齐和标注。f1_score 默认按实体级计算,不要用 sklearn 的 token-level accuracy 替代。

5. 避坑与排查:标签错位、F1 为 0、显存爆掉时先看这几处

NER 微调的故障很少是“模型不够强”,多数是数据链路错位。下面 5 条是实际复现时最容易踩的坑,每条按现象、原因、解决写,排查顺序从数据到模型再到显存。

5.1 现象:训练 loss 正常但验证集 F1 接近 0

现象是训练 loss 从 2 降到 0.2,验证集 F1 只有 0.01,classification_report 里所有实体都是 0。原因通常有三类:labels 和 input_ids 错位、验证集标签全是 O、label2id 映射把 O 和 B- 弄反。先抽一条验证样本,把 input_ids、tokens、labels 打出来,人工对一遍。

sample = val_dataset[0] tokens = tokenizer.convert_ids_to_tokens(sample['input_ids']) for t, l in zip(tokens, sample['labels']): print(repr(t), l, id2label.get(l, 'IGNORE'))

如果 tokens 和 labels 对不上,回到 get_token_label 检查断言是否被注释。如果 labels 全是 0,检查 char_label.txt 是否读错文件。如果标签 id 整体偏移,检查训练时 num_labels 和 label2id 是否同一套。解决方式是把对齐样例固化成单元测试,训练前必须跑通一条带实体的短句。

5.2 现象:推理输出的实体边界总是多一个字或少一个字

现象是模型能把“乙公司”识别成 company,但输出成“乙公”或“乙公司工”。原因多出在对齐阶段:[UNK] 继承标签时多推了一个字符,subword 的##没去掉,或者特殊 token 被赋了 B-。先打印推理 tokens 和预测标签,看边界落在哪个 token 上。

for t, p in zip(tokens, pred_labels): print(repr(t), p)

如果 B-company 落在“乙”,I-company 落在“公”,但“司”变成 O,检查 char_label 里“司”是否标成 I-company。如果英文 subword 把##ing标成 B-,在对齐函数里去掉##后逐字符推进,不要让 subword 整体继承 B-。解决方式是给 get_token_label 加更多断言:每个 token 必须有标签,token_labels 长度等于 tokens 长度,特殊 token 必须是 O。

5.3 现象:batch size 一调大就 OOM,调小又不收敛

现象是 batch size 16 直接显存溢出,调到 2 后 loss 震荡。原因通常是 max_len 设得过大、动态 padding 没用、或者模型没开梯度累积。先看句长分布,95% 的句子在 128 以内就把 max_len 降到 128;再开梯度累积,把等效 batch 拉回来。

training_args = { 'per_device_train_batch_size': 4, 'gradient_accumulation_steps': 4, 'max_len': 128 }

per_device_train_batch_size=4降低单步显存,gradient_accumulation_steps=4等效 batch 16。max_len 从 256 降到 128 通常能省一半以上显存。如果还 OOM,检查是否把 logits 和 labels 都留在显存里,评估时用torch.no_grad()。解决顺序是降长度、降 batch、开累积,不要一上来换小模型。

5.4 现象:保存再加载后 label2id 对不上

现象是训练时 F1 0.8,保存后重新加载推理,实体全乱。原因多是只保存了 state_dict,没有保存 config 里的 id2label;或者推理脚本自己手写了一套 label2id,顺序和训练时不同。解决方式是统一用 save_pretrained 保存模型和 tokenizer,加载后从 config 读映射。

model.save_pretrained('./ner_model') tokenizer.save_pretrained('./ner_model') from transformers import BertForTokenClassification loaded = BertForTokenClassification.from_pretrained('./ner_model') assert loaded.config.id2label[0] == 'O' assert loaded.config.label2id['B-company'] == label2id['B-company']

save_pretrained会把 model config、权重、tokenizer 词表一起落盘。加载后断言 O 在 0、B-company 的 id 与训练一致。如果 config 里没有 id2label,手工补一个 label2id.json 并在推理入口强制校验,不要靠记忆。

5.5 现象:中文标点/英文/数字混排时 [UNK] 把标签吃掉

现象是含生僻字或英文的句子,实体中间突然断掉,[UNK] 附近标签变成 O。原因是 tokenizer 把生僻字映射成 [UNK],对齐函数如果没有继承当前字符标签,就会给 O;英文被拆成 subword 时,##片段也可能被特殊 token 分支误判。先统计 [UNK] 比例,再决定是换词表还是改对齐。

unk_id = tokenizer.convert_tokens_to_ids('[UNK]') unk_ratio = sum(ids.count(unk_id) for ids in all_input_ids) / sum(len(ids) for ids in all_input_ids) print('unk ratio:', unk_ratio)

unk_ratio超过 1% 就要看数据里是否有大量生僻字、特殊符号、表情。对齐层面,[UNK] 必须继承当前字符标签,不能直接给 O;subword 去掉##后逐字符匹配。如果业务文本里英文和数字很多,考虑自定义 normalize 或换更匹配的词表。解决不了 [UNK] 时,至少把 [UNK] 位置从评估里排除,避免 F1 被噪声拉低。

6. 把模型推到可用:从单条推理到批量校验的最后一个技巧

训练完不等于能用。推理脚本要解决两个问题:把 token 级预测还原成实体片段,以及在批量文本上做一致性校验。下面这段代码先对齐 token,再按 B- 开新实体、I- 延续、O 关闭,输出实体文本和类型。

import torch def predict(text, model, tokenizer, id2label, max_len=128): enc = tokenizer(text, return_tensors='pt', truncation=True, max_length=max_len) with torch.no_grad(): logits = model(**enc).logits pred_ids = logits.argmax(-1)[0].tolist() tokens = tokenizer.convert_ids_to_tokens(enc['input_ids'][0]) entities, cur_type, cur_text = [], None, '' for tok, pid in zip(tokens, pred_ids): if tok in tokenizer.all_special_tokens: continue label = id2label[pid] if label.startswith('B-'): if cur_type: entities.append((cur_type, cur_text)) cur_type, cur_text = label[2:], tok.replace('##', '') elif label.startswith('I-') and cur_type == label[2:]: cur_text += tok.replace('##', '') else: if cur_type: entities.append((cur_type, cur_text)) cur_type, cur_text = None, '' if cur_type: entities.append((cur_type, cur_text)) return entities

return_tensors='pt'让输入直接进模型,torch.no_grad()关掉梯度。argmax(-1)取每个 token 的最大概率标签。后处理按 B- 开新实体、I- 同类型延续、其他标签关闭。tok.replace('##', '')还原 subword。参数上,max_len 要和训练一致;id2label从 model.config 读,不要手写。

上线前我会做三行检查:第一,抽 20 条验证集样本,打印 tokens、真实标签、预测标签,人工看边界;第二,统计每类实体的 support 和 F1,support 为 0 的类不要写进报告;第三,把 label2id 从 checkpoint 回读,和训练脚本的字典做相等断言。批量校验可以用混淆矩阵看 B/I 混淆,尤其是 B-company 被预测成 I-company 的情况,通常说明实体起始边界学偏了。

| 检查项 | 看什么 | 不通过时先改哪 |

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/2 11:46:45

人工势场法与CBF在机器人路径规划中的应用与优化

1. 项目背景与核心需求 在机器人导航和自动驾驶领域,路径规划是最基础也最关键的环节之一。想象一下,当你驾驶车辆穿过拥挤的停车场时,大脑需要实时处理周围车辆、行人和障碍物的位置信息,同时规划出一条安全高效的路线——这正是…

作者头像 李华
网站建设 2026/11/2 11:43:38

Clonezilla Live整盘备份还原与网络批量部署实战指南

简介:这份Clonezilla Live 2.7.0-10 AMD64镜像面向需要做系统迁移、分区备份、灾难恢复与个人数据保护的运维人员及进阶用户,也适合服务器维护和机房批量部署场景。它基于Linux开源方案,支持ext2/3/4、FAT、NTFS等多种文件系统,并…

作者头像 李华
网站建设 2026/11/2 11:42:56

性能指标全解析:从基础概念到优化实践

1. 性能指标概述:从理论到实践的认知框架 在技术领域摸爬滚打十几年,我见过太多团队在性能优化这件事上栽跟头——不是过早陷入局部优化,就是抓不住关键瓶颈。性能指标就像医生手中的听诊器,用对了能精准定位问题,用错…

作者头像 李华
网站建设 2026/11/2 11:41:53

Redis安装与配置指南:Windows与Linux环境实战

1. Redis安装概述:为什么选择它? Redis作为当前最流行的内存数据库之一,其读写性能可达10万/秒的QPS,特别适合缓存、会话存储和实时排行榜等场景。我在电商秒杀系统和高并发API项目中多次验证过它的稳定性——单节点轻松支撑5万并…

作者头像 李华
网站建设 2026/11/2 11:40:13

Sublime Text快捷键全解析:提升编码效率的终极指南

1. Sublime Text快捷键全解析:从基础到高阶实战 Sublime Text作为一款轻量级但功能强大的代码编辑器,其快捷键系统是提升编码效率的核心武器。我使用Sublime Text已有七年时间,从最初只会用CtrlS保存文件到现在可以完全脱离鼠标操作&#xff…

作者头像 李华
网站建设 2026/11/2 11:40:04

Partitioned Cookie技术解析:原理、实现与应用

1. Partitioned Cookie 技术背景与核心概念 在传统Web架构中,Cookie作为维持用户会话状态的核心机制已存在二十余年。当用户访问嵌入了第三方资源的网站时,这些第三方服务可以通过Cookie实现跨站用户追踪,形成了所谓的"第三方Cookie&quo…

作者头像 李华