尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
中文NER实战:bert-base-chinese微调与标签对齐
简介面向自然语言处理初学者与需要动手实现实体抽取任务的开发者这篇 PDF 文档围绕使用 BERT 预训练模型微调完成命名实体识别NER展开属于可直接参考的实操笔记。内容先梳理 NER 与 BERT 的理论背景再以地址、书籍、公司、游戏、政府、电影、姓名、组织、职位、场景十类实体为例给出实体定义、B-/I- 标签编码与 id2label、label2id 映射的完整代码随后演示 BertTokenizer 中文分词、input_ids/token_type_ids/attention_mask 三个输入字段的含义以及分词后 token 与原文长度不一致时的标签对齐处理思路并涉及线性分类层搭建、训练评估指标与预测应用。资源包共 1 个 PDF 文件约 545KB篇幅紧凑便于通读已有 1245 人学习下载适合希望从零跑通中文 NER 流程、对照代码复现实验的读者。1. 中文 NER 为什么要从 BERT 微调起步手上有一批带标注的中文句子要把里面的人名、公司名、书名、职位抽出来入库第一反应往往是拼一套 BiLSTM-CRF。可标注数据一旦只有几千句这东西很容易欠拟合调参调到怀疑人生。用 bert-base-chinese 做微调来实现命名实体识别收敛快、字段清楚哪怕是第一次接触 transformers 的人照着改也能跑出可用的基线。这份材料适合两类人一类是要做信息抽取、知识图谱入库、合同要素提取的开发者需要一条能立刻跑通的中文 NER 链路另一类是刚上手 BERT 微调、想弄明白标签到底怎么对到 token 上的人。整份材料里最值钱的不是模型结构——那部分 transformers 已经封装好了——而是那段 get_token_labelBERT 的分词结果和原文字符天然对不齐这一步做错后面训练再久也是白搭。2. 技术选型与工程结构bert-base-chinese 微调的完整链路先定方案再动手。中文 NER 的常见起手式有四种选哪个基本取决于你手上标注数据的量级和对线上延迟的要求方案标注数据要求训练成本中文效果适合什么场景字向量 BiLSTM-CRF万句以上低中没有预训练资源、要私有部署的老系统BERT 线性分类头千句级起中高绝大多数业务 NER这份材料走的就是这条BERT CRF 层千句级起中偏高略高标签转移约束强、BIO 序列不能乱跳的场景通用大模型 提示词几十条示例推理成本高高但不可控冷启动打样、验证实体体系是否合理这份材料选的是第二条。理由很实在bert-base-chinese 的词表是按字切的中文句子里几乎每个汉字都对应一个 token[UNK] 出现得极少预训练阶段已经吃下了大量中文语料微调阶段只需要让它学会这个字该归到哪一类标签3 到 5 个 epoch 就能看到像样的 F1。相比之下CRF 层带来的增益在实体边界本来就清晰的语料上并不明显反而多一层调参负担所以我一般先把它留作后手。模型结构本身没什么花活12 层 Transformer 编码器输出 768 维向量接一个 dropout再接一个Linear(768, num_labels)的分类头对每个 token 位置做独立分类损失用 token 级交叉熵padding 位置用-100忽略掉。听起来简单但对每个 token 位置分类这句话背后有个前提——你的标签必须已经和 token 一一对齐了。整篇文章的大半篇幅都会花在这个前提上。2.1 一条样本从 text.txt 走到 checkpoint 经过哪几步把链路摊开看一份原始语料要变成可推理的模型中间有七个环节缺一环都会在训练时报出莫名其妙的错语料读取从text.txt和char_label.txt按空行分段得到句子列表和字符级标签列表标签体系构建把实体清单展开成OB-*I-*的 21 个标签建立label2id/id2label双向映射分词用BertTokenizer把中文句子切成 token 并转成input_ids标签对齐把字符级标签搬到 token 级得到与input_ids等长的labels样本封装把input_ids、token_type_ids、attention_mask、labels打包成一条样本批处理与训练DataCollatorForTokenClassification负责动态 paddingTrainer跑反向传播评估与推理用 seqeval 算实体级 F1用argmax解码出实体片段。第 4 步是整个链路里唯一需要自己写逻辑的地方其余六步基本都是调库。这也是为什么很多教程写到加载模型、开始训练就带过了——真正会翻车的地方被藏起来了。2.2 依赖清单与目录骨架环境不复杂四个包就够但要留意版本。transformers 4.x 之后Trainer的 API 有过变动evaluation_strategy在较新版本里改名为eval_strategy装的时候最好锁一下版本pip install torch transformers datasets seqeval accelerate我习惯把代码按职责拆成几个小文件而不是全塞进一个 notebook。理由是标签定义、对齐逻辑、训练脚本三者改动频率完全不同——换实体体系只动第一个调整对齐策略只动第二个调超参只动第三个拆开以后改起来不用通读全文文件职责改动频率label_def.py实体清单、label_names、id2label/label2id换业务时改data_loader.py读取text.txt/char_label.txt、语料校验换数据格式时改align.pyget_token_label、make_sample基本不动train.pyDataset、Collator、模型、Trainer、评估指标调参时改predict.py单句推理、实体片段还原上线时改outputs/checkpoint、tokenizer 副本、日志不用管这种拆法还有个隐性好处align.py里的对齐函数可以被训练脚本和推理脚本同时引用保证训练时怎么对齐、推理时就怎么切分不会出现训练用一套规则、上线用另一套规则的经典事故。2.3 为什么不在 BERT 上面直接堆 CRFCRF 解决的是标签转移合法性问题比如I-company前面不该出现OB-book后面跟I-name属于非法跳转。如果你的语料标注质量不高、BIO 序列本身就乱CRF 确实能兜住一部分错误。但在中文 NER 的常规场景里我见过更多的情况是实体边界错不是因为转移不合法的而是因为模型压根没学到那个字的语义。这时候加 CRF 层只是把错误从标签跳转变成了整段概率被压低指标涨个零点几个点调参成本倒是翻倍。所以我的建议是先把 BERT 线性头跑通看 seqeval 的 F1 涨到多少再决定要不要加 CRF。如果 F1 卡在 0.7 以下问题多半在语料和对齐上不在模型结构上。3. 标签体系与语料读取21 类 BIO 编码与双文件解析标签体系是整个项目的地基定错了后面全得返工。这份材料里定义的实体类型有十种# label_def.py 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] # 双向映射训练时把字符串标签转成 id评估和解码时再转回来 id2label {i: name for i, name in enumerate(label_names)} label2id {v: k for k, v in id2label.items()} NUM_LABELS len(label_names) # 1 10 * 2 21O放在索引 0 是有讲究的。它是出现频率最高的标签占语料里九成以上的 token把它固定成 0 号在做混淆矩阵和抽样检查时看着舒服。更重要的是-100是 PyTorch 交叉熵的默认忽略索引-100不能是合法标签 id这点在配label_pad_token_id的时候要格外确认。3.1 实体清单展开成 21 个 BIO 标签10 种实体每种两个前缀加上一个O一共 21 类。展开逻辑看着平淡但这里有个容易忽略的细节B-和I-的排列顺序决定了 id 的分布。上面的写法是先把所有B-排完再排所有I-好处是同一实体的 B 和 I 相差 10做调试的时候能一眼看出某个 id 属于哪个实体类型。如果你更习惯B-company和I-company挨在一起的排法也能跑只是调试时不能靠心算推 id得老老实实查id2label。两种排法在模型效果上没有区别别在这上面纠结。真正要注意的是NUM_LABELS这个常量。它必须和label_names的长度严格一致而label_names又依赖entities的长度。所以换业务的时候只改entities列表NUM_LABELS会跟着自动变但如果你在别处手写了num_labels21那就是个定时炸弹——语料扩展成 15 类实体以后模型输出层维度对不上报的错是size mismatch找起来很费时间。3.2 text.txt 与 char_label.txt 的双文件解析这份材料的语料是双文件结构一个文件存原文一个文件存字符级标签两边按空行分段同一段落里的行拼成一句话。读取时不要一上来就逐行当句子处理空行才是切分点# data_loader.py def load_texts(file_path): 按空行分段返回句子字符串列表 texts, buf [], [] with open(file_path, r, encodingutf-8) as f: for line in f: line line.strip() if line: buf.append(line) # 非空行拼接到当前句子 elif buf: texts.append(.join(buf)) buf [] # 遇到空行当前句子结束 if buf: texts.append(.join(buf)) # 处理最后一句没有空行收尾的情况 return texts def load_labels(file_path): 按空行分段每行一个字级标签返回二维列表 all_labels, buf [], [] with open(file_path, r, encodingutf-8) as f: for line in f: line line.strip() if line: buf.append(line) elif buf: all_labels.append(buf) buf [] if buf: all_labels.append(buf) return all_labels两个函数的结构几乎一模一样但返回值形态不同texts是字符串列表labels是列表的列表。差别在于文本要拼成一整句才能送进 tokenizer标签则必须保持每个字符一个标签的粒度方便后面按字符游标去取。两个函数末尾那段if buf都不能省。真实语料里最后一句大概率没有空行收尾少了这两行最后一条样本会静默丢失。丢一条不会报错只是训练集少了一条你几乎不可能发现——直到某天做数据统计时才发现句子数和标签数差了 1。这种沉默的数据丢失比报错烦人十倍。3.3 读进来的第一件事是校验不是训练读完数据立刻校验别急着进训练循环。我一般加三层断言成本几乎为零但能拦住八成的后续事故def check_alignment(texts, labels, label2id): # 1. 句子数必须一致 assert len(texts) len(labels), \ f句子数不一致: text{len(texts)}, label{len(labels)} for i, (t, ls) in enumerate(zip(texts, labels)): # 2. 每个句子字符数必须等于标签数 assert len(t) len(ls), \ f第 {i} 句字符数 {len(t)} 与标签数 {len(ls)} 不匹配 # 3. 标签必须都在映射表里 unknown set(ls) - set(label2id) assert not unknown, f第 {i} 句出现未定义标签: {unknown}第 2 条断言是最有用的。字符级语料最常见的错误是标点符号被漏标或多标句子里有个全角逗号标签文件里没有对应那一行长度就差 1。这种错位如果不查会在标签对齐阶段一路传下去最终表现为模型在某些句子上永远预测错而你盯着代码看半天看不出问题。第 3 条断言同样值得写。这份材料的实体清单里定义的是company、book这类英文类型名但示例语料里实际用的是B-Apparel——清单和语料对不上是这类项目最高频的翻车点后面排查章节会详细讲。4. 分词对齐与样本构造get_token_label 四条分支逐行拆解这一章是整个项目的核心也是最容易写错的地方。前面标签定义得再漂亮如果对齐这一步错了训练出来就是一个看起来很正经但什么都识别不出的模型。4.1 BertTokenizer 对一句中文到底做了什么拿一句短的中文句子试试看看 tokenizer 吐出了什么from transformers import BertTokenizer tokenizer BertTokenizer.from_pretrained(bert-base-chinese) text 张某就职于某某科技有限公司 enc tokenizer(text) print(enc[input_ids]) print(enc[token_type_ids]) print(enc[attention_mask]) print(tokenizer.convert_ids_to_tokens(enc[input_ids]))input_ids是文本的编码序列每个 token 对应词表里的一个唯一 id模型只认这个。token_type_ids用来区分句子对任务里的第一句和第二句单句 NER 场景下全是 0留着就行。attention_mask指示哪些位置是有效 token、哪些是 padding有效位是 1、padding 位是 0——BERT 的注意力机制会读这个掩码把 padding 位置的权重压掉。convert_ids_to_tokens把 id 还原成字符输出大概长这样[[CLS], 张, 某, 就, 职, 于, 某, 某, 科, 技, 有, 限, 公, 司, [SEP]]15 个 token原始句子 13 个字符。多出来的两个是[CLS]和[SEP]它们是 BERT 强制加的头尾标记不参与任何实体。如果你的标签列表只有 13 个直接塞给模型长度对不上第一轮就崩。4.2 分词长度和字符长度对不上的四类原因总结下来token 数不等于字符数基本跑不出这四种情况特殊标记。[CLS]和[SEP]一定会插进来每条样本固定多 2 个 token。这两个位置的标签统一给O。英文 subword 切分。中文句子夹英文单词时wordpiece 会把不认识的词拆开后半截带##前缀。一个 6 个字母的单词可能变成 2 到 3 个 token而这几个 token 共享同一个字符级标签。[UNK] 替换。词表里没有的字符会被替换成[UNK]一换一长度不变但字符信息丢了。中文场景下这种情况少遇到生僻字或特殊符号时会出现。大小写和全半角归一化。这一条最阴。BertTokenizer内部会做 basic tokenize某些字符会被归一化如果你的代码里又手动text.lower()了一次大小写转换的字符数和原文就对不上了。中文语料里偶尔夹一个英文缩写就能触发这个错位。后两种属于隐性错位不会报长度错误只会让标签整体偏移一位——模型照样能训练只是学到的对应关系全乱了最终表现为 F1 卡在很低的位置上不去。这也是为什么我坚持在get_token_label末尾加一句assert len(token_labels) len(tokens)。4.3 get_token_label 的四条分支逐行看对齐函数的目标很明确遍历 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()) # 注意这里的小写转换是隐患见 5.4 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 token_labels.append(char_tp) try: char next(iter_text) char_tp next(iter_char_label) except StopIteration: pass # 分支二特殊 token[CLS]/[SEP]统一标 O elif t in tokenizer.special_tokens_map.values() and t ! [UNK]: token_labels.append(O) # 分支三[UNK] 占一个字符位继承当前字符标签并推进游标 elif t [UNK]: token_labels.append(char_tp) try: t next(iter_tokens) except StopIteration: break 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 # 分支四多字符 token英文 subword整段共享首个字符的标签 else: t_label char_tp t t.replace(##, ) for c in t: 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 assert len(token_labels) len(tokens) # 长度对不上就立刻炸别拖到训练 return token_labels分支一是主路径。中文汉字在bert-base-chinese词表里基本都是一字一 token走这条分支时 token 和字符严格一一对应assert t char相当于给整段对齐上了一道校验——只要这里没炸说明前面的游标推进是对的。分支二处理特殊 token。注意条件里排除了[UNK]因为[UNK]在special_tokens_map里也算特殊 token但它需要继承标签而不是给O所以必须先排除掉再走分支三。分支三处理[UNK]。它占一个字符位所以给当前字符的标签然后推进到下一个 token。后面那段while char ! t[0]是重新同步游标——因为[UNK]可能代表多个字符被吞需要一直推进到当前字符和下一个 token 的首字符对上为止。这段逻辑是整份代码里最绕的地方看不太懂就先把[UNK]的命中次数打印出来如果一条语料里[UNK]出现几十次说明词表覆盖不够该考虑换模型或者做字符清洗了。分支四处理英文 subword。##是 wordpiece 的续接标记要先去掉才能和原文字符比对。整段 subword 共享同一个标签这在 BIO 体系下没问题——因为B-和I-是按字符位置标的一个英文单词内部的字符标签本来就该一致。if t_label ! O那个判断实际上是个空操作赋的还是char_tp读的时候可以直接跳过。对齐完成后的结果非常直观把 token 和标签并排打出来就能核对for t, t_label in zip(tokens, token_labels): print(t, \t, t_label) # [CLS] O # 张 B-name # 某 I-name # 就 O # 职 O # 于 O # 某 B-company # 某 I-company # 科 I-company # ... # [SEP] O我一般会抽十条样本肉眼过一遍重点看[CLS]、[SEP]是不是都给了O英文单词是不是整段标签一致句子首尾的实体有没有被切掉。这十分钟的检查能省掉后面几个小时的 debug。4.4 make_sample把对齐结果拼成训练样本对齐函数只返回标签字符串列表还需要转成 id 并和 tokenizer 的输出打包在一起def make_sample(text, char_label, tokenizer, label2id): sample tokenizer(text) # input_ids / token_type_ids / attention_mask token_label get_token_label(text, char_label, tokenizer) # token 级标签字符串 sample[labels] [label2id[x] for x in token_label] # 转成 id return sample返回的字典里四个字段缺一不可。labels的长度必须和input_ids完全相等这是BertForTokenClassification内部做交叉熵时的硬性要求。任何一条样本长度不匹配Trainer会在那个 batch 上报维度错误而且报错信息里不告诉你是第几条样本只能靠assert提前拦住。构造完样本列表以后建议做一次抽查samples [make_sample(t, l, tokenizer, label2id) for t, l in zip(texts, labels)] for s in samples[:3]: assert len(s[input_ids]) len(s[labels]) print(len(s[input_ids]), s[labels][:10])打印出来的labels前几个应该都是 0[CLS]的O中间偶尔冒出 1、2B-*、I-*。如果前十个全是 0 而且整句都是 0说明这句确实没有实体正常如果整批语料的labels都是 0那基本可以确定标签读取路径写错了比如读成了另一个文件或者空行切分逻辑把标签吞了。5. 训练评估与常见问题排查到这里数据和样本都准备好了接下来是标准流程包 Dataset、配 Collator、起 Trainer、算指标。这几步代码量不大坑却不少。5.1 Dataset 与 DataCollatorForTokenClassificationdatasets.Dataset.from_list可以直接把样本列表转成 Dataset字段名保持input_ids/token_type_ids/attention_mask/labels就行Collator 认这几个名字from datasets import Dataset from transformers import DataCollatorForTokenClassification ds Dataset.from_list(samples) ds ds.train_test_split(test_size0.1, seed42) # 固定 seed保证每次划分一致 collator DataCollatorForTokenClassification( tokenizertokenizer, label_pad_token_id-100, # padding 位置的标签设为 -100交叉熵会忽略 )label_pad_token_id-100这个参数必须显式写。默认值也是-100但显式写出来是为了防止有人后来改配置时把它覆盖成 0——一旦变成 0padding 位置就被当成O标签参与训练模型会学到越长的句子越该输出 O长实体识别率直接崩。Collator 做的是动态 padding同一个 batch 内的样本补到该 batch 最长长度而不是全局最长。这样短句多的 batch 显存占用小训练速度能快一截。代价是不同 batch 的序列长度不一致如果你要导出 ONNX 或者做固定 shape 的推理得另外处理。5.2 模型头、超参和训练入口模型加载时把id2label和label2id一起传进去好处是保存 checkpoint 时会写进config.json推理时不用再手动对一遍标签顺序from transformers import BertForTokenClassification, TrainingArguments, Trainer model BertForTokenClassification.from_pretrained( bert-base-chinese, num_labelsNUM_LABELS, id2labelid2label, label2idlabel2id, ) args TrainingArguments( output_diroutputs/ner-bert, per_device_train_batch_size16, per_device_eval_batch_size32, learning_rate2e-5, # 微调 BERT 的常规起点别超过 5e-5 num_train_epochs4, # 千句级语料 3~5 轮足够再多就过拟合 warmup_ratio0.1, weight_decay0.01, logging_steps50, save_strategyepoch, load_best_model_at_endTrue, metric_for_best_modelf1, )学习率是最值得先调的一个参数。2e-5是微调 BERT 类模型的通用起点语料少于两千句时可以降到1e-5防止过拟合语料上万句时可以升到3e-5加快收敛。num_train_epochs给 4 轮是个保守值如果第 2 轮 eval F1 就开始掉说明模型已经在背训练集了早点停。metric_for_best_modelf1这一行要配合下面的compute_metrics才有意义。它决定了load_best_model_at_end到底加载哪一轮的权重——如果指标名写错Trainer 会静默加载最后一轮你可能在用最差的那个 checkpoint 做推理。5.3 seqeval 算出的实体级 F1 才是能看的指标NER 的评估千万别用 token 级 accuracy。语料里九成以上的 token 都是O模型全预测O也能拿到 0.9 的准确率毫无意义。要用 seqeval 算实体级的精确率、召回率和 F1import numpy as np from seqeval.metrics import precision_score, recall_score, f1_score def compute_metrics(eval_pred): logits, labels eval_pred preds np.argmax(logits, axis-1) true_labels, true_preds [], [] for p_row, l_row in zip(preds, labels): cur_t, cur_p [], [] for p, l in zip(p_row, l_row): if l -100: # 跳过 padding 位置绝不能拿去查 id2label continue cur_t.append(id2label[l]) cur_p.append(id2label[p]) true_labels.append(cur_t) true_preds.append(cur_p) return { precision: precision_score(true_labels, true_preds), recall: recall_score(true_labels, true_preds), f1: f1_score(true_labels, true_preds), }if l -100: continue这一行是必须的。padding 位置的标签是-100id2label[-100]在 Python 里不会报错它会从字典末尾倒着取——取到某个莫名其妙的标签然后 seqeval 就会把这些垃圾标签算进分母F1 被拉低而你找不到原因。这种 bug 极难排查因为程序不崩只是指标不对。seqeval 默认用的是 strict 模式要求B-X后面必须跟I-X才算一个完整实体B-X直接跟O也算对但B-X跟I-Y会被判错。如果你的语料标注本身比较松散可以切到modelenient但我不建议——strict 模式暴露出来的问题往往正是语料该修的地方。5.4 五个翻车现场现象、原因、解决下面这五条是我在这个链路上真实遇到过、并且看到别人也反复踩的。现象一加载语料后直接KeyError: B-Apparel。原因entities清单里定义的是company、book这些类型但语料实际标的是B-Apparel之类。label2id是按定义清单生成的语料里出现的标签不在其中[label2id[x] for x in token_label]当场炸。 解决读入语料后先统计真实标签集合和label2id.keys()求差集差集非空就停下来。要么把entities改成语料的实际类型要么写一张映射表把语料标签归并到目标体系比如Apparel归并进book。这一步必须在分词对齐之前做完别等到训练才发现。现象二训练 loss 一直不降或者降下去以后 F1 接近 0。原因标签整体错位了一位。最常见的是漏掉了[CLS]导致第一个字继承到了[CLS]的O后面全部平移一格也可能是text.lower()和 tokenizer 内部的大小写处理打架字符游标提前推进。 解决把assert len(token_labels) len(tokens)加在get_token_label末尾同时在make_sample里断言len(sample[input_ids]) len(sample[labels])。然后抽一条样本把 token 和 label 并排打印出来逐个人眼核对十有八九能一眼看出错位起点。现象三训练集 F1 很高验证集很低差 20 个点以上。原因语料本身太少或者训练集和验证集是按文件切的而不是随机切的——如果text.txt是按主题排过序的切出来的验证集和训练集分布完全不同。 解决用train_test_split(seed42)做随机划分并且检查划分后两边的实体类型分布是否接近。千句级语料下验证集至少留 100 条否则 F1 波动会大到没有参考价值。现象四长句子里句子后半段的实体永远识别不出来。原因超过max_length512被截断了而且labels没有跟着一起截断或者截断了但你没意识到。BertTokenizer默认不截断tokenizer(text)会返回完整长度超过 512 的样本进模型时位置编码越界。 解决在make_sample里显式指定truncationTrue, max_length512然后按len(sample[input_ids])同步截断labels。更好的做法是在语料预处理阶段按标点把长句切成子句切分处的标点标O这样不会丢实体。现象五trainer.train()跑完load_best_model_at_end好像没生效。原因metric_for_best_model写的指标名和compute_metrics返回的键名不一致比如返回的是f1配置里写的是eval_f1Trainer 找不到这个指标静默回退到最后一轮。也可能是greater_is_better没设成True。 解决把compute_metrics的返回键名统一加上eval_前缀或者显式传greater_is_betterTrue跑完后打印trainer.state.best_model_checkpoint确认加载的确实是最优轮次而不是最后一个 step。6. 二次开发换实体体系、接 CRF、换小模型和上线前的验证套路真正上手改的时候动作基本集中在三处改造目标改哪个文件具体动作换实体体系label_def.py改entities列表重建label2id同步语料标签映射加 CRF 层train.py用Bert Linear CRF替换BertForTokenClassification损失和解码都走 CRF换小模型train.py把bert-base-chinese换成更小的中文预训练权重num_labels保持不变上线推理predict.py加载 checkpoint 时一并加载 tokenizer保证分词规则与训练一致换实体体系看着只需要改一个列表实际最麻烦的是语料。旧的标注文件里可能混着一批已经废弃的实体类型直接删掉会丢失信息直接保留又和新的label2id对不上。我的做法是写一张old_label - new_label的映射表把废弃类型统一映射成O跑一遍离线转换生成新的char_label.txt然后再进训练流程。这样原始标注文件不用动出问题还能回滚。加 CRF 的收益要辩证看。如果你的实体边界问题确实严重比如B-company后面经常跟出I-name这种非法跳转那 CRF 值得加。实现上有现成的封装把 BERT 输出接一个nn.Linear(hidden, num_labels)再接torchcrf.CRFforward返回损失decode返回最优路径。代价是训练速度会慢一截因为 CRF 的前向计算没法本文还有配套的精品资源点击获取
RELATED

相关推荐

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

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

简介:面向自然语言处理与BERT入门进阶学习者的实操型PDF资料,聚焦中文命名实体识别(NER)的微调落地:从地址、公司、姓名、职位等10类实体与B-/I-标签编码,到BertTokenizer中文分词、input_ids/token_type_i…

📅 2026/11/2 11:49:23
MacPorts 管理 macOS 多版本 Python:安装、查询与默认切换

MacPorts 管理 macOS 多版本 Python:安装、查询与默认切换

在 macOS 上同时维护 python2 和 python3 的多个小版本,然后用 port 这套包管理工具去查询、安装、切换默认版本,是我这几年接手老项目时反复要走的一条路。它解决的问题很具体:系统自带的解释器不能动、不同项目要求的 3.8/3.10/3.12 互相打…

📅 2026/11/2 11:49:23
2026年程序员薪资趋势与网络安全岗位解析

2026年程序员薪资趋势与网络安全岗位解析

1. 2026年程序员薪资全景概览 2026年的技术行业呈现出明显的两极分化趋势:基础开发岗位薪资增长放缓,而网络安全、AI工程等前沿领域薪资涨幅持续领跑。根据对国内主流招聘平台和猎头渠道的抽样统计,程序员整体薪资中位数达到28K/月&#xff0…

📅 2026/11/2 11:44:22
MORE NEWS

更多资讯

📰

从企业微信三层对接,到「避坑 vs 寻优」的对比——一个 Agent 的自学笔记

原文链接:https://mp.weixin.qq.com/s/w5YewCcxNJ8SE8udZ3LpqA 免责声明:本文为月牙寂(龚浩华)与月绡凝的个人 AI 工程化实践探索记录,仅供学习参考,不构成任何商业建议、技术承诺,也不构成对企…

📰

千问办公,如何成为组织生产力底座?

企业办公软件越来越多,效率却未必同步增长。 查资料要开浏览器,确认事项要翻群聊,整理数据要进表格,写报告要用文档,做汇报又切到 PPT,最后还可能需要制作网页、海报或客户展示材料。每个工具都解决了一个…

📰

银行网点客户中心:体验家XMPlus的客户档案与反馈追踪

摘要同一客户可能在多个网点、不同数字渠道和多次回访中留下反馈。若只按答卷逐条查看,就难以理解体验如何变化;若身份关联过度,又可能把不同客户的经历混在一起。本文围绕客户主档、服务记录、反馈和处理时间线,说明身份映射、标…

📰

DeepSeek LeetCode 477. 汉明距离总和 Java实现

思路 按位统计:两个数在某一位上不同,这一位就对汉明距离贡献 1。 对于第 i 位:统计 nums 中该位为 1 的个数 ones该位为 0 的个数为 n - ones任意一个「1」和一个「0」的组合都会在这一位上产生 1 个距离,所以这一位对总和…

📰

DeepSeek LeetCode 478. 在圆内随机生成点 Java实现

LeetCode 478. 在圆内随机生成点 核心思路 关键点在于「均匀」二字。很多人第一反应是: // ❌ 错误做法 double r radius * Math.random(); double theta 2 * Math.PI * Math.random();这样会导致点集中在圆心附近。原因是面积元是 dA rdrdθ,半径越大…

📰

抖音 TikTok 作品批量下载与账号数据归档:DouK-Downloader 交付物清单

抖音 TikTok 作品批量下载与账号数据归档:DouK-Downloader 交付物清单 【免费下载链接】TikTokDownloader 抖音 / TikTok 平台作品下载/数据采集工具 项目地址: https://gitcode.com/GitHub_Trending/ti/TikTokDownloader DouK-Downloader(曾用名…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬