十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

PyTorch实现BERT-BiLSTM-CRF命名实体识别系统详解

PyTorch实现BERT-BiLSTM-CRF命名实体识别系统详解 简介本资源是一套基于PyTorch实现的BERT-BiLSTM-CRF命名实体识别NER完整项目面向计算机专业毕业设计学生与NLP实战开发者解决中文文本中人名、地名、机构名等实体精准识别问题。项目采用预训练BERT提取深层语义特征BiLSTM建模上下文依赖CRF层优化标签序列全局一致性技术路线清晰、结构严谨已通过答辩并获98分高分评价。压缩包共34个文件4.69MB含6个核心Python模块如model.py、crf.py、main.py、6个文本数据集train/dev/test及Bio格式标注、5个XML配置与IDE工程文件、2个Jupyter Notebook含数据处理与预测演示、README.md说明文档及BERT中文预训练模型子目录模块化设计便于理解各阶段功能。目前已有68人学习下载提供从数据预处理、模型构建、训练调优到评估预测的全流程可运行代码附带详细注释与本地验证通过的配置是掌握前沿神经网络统计模型融合NER方案的优质实践材料。 最近在做一个中文信息抽取的项目需要从非结构化的简历文本里抽取出人名、机构名、地点这类实体。试过正则、词典匹配规则写了一大堆换一个文本形态就失灵召回率很难看。后来直接把方案改成了基于PyTorch的BERT-BiLSTM-CRF命名实体识别NER系统效果确实立竿见影。不光是在简历场景像法律文书、医疗病历、商品评论这些垂直领域这套组合都是目前性价比最高的基线方案。这篇文章就把整个系统的实现过程、源码逻辑、数据准备和踩过的坑完整记录下来适合刚接触NER、想快速跑通BERT系列模型或者正打算改造自己规则系统的朋友做参考。先交代一下项目的大背景。命名实体识别是NLP里最经典的信息抽取任务之一目标是从一段非结构化文本中识别出具有特定意义的实体边界和类型比如人名PER、地名LOC、组织机构ORG、时间TIME等。它是知识图谱构建、关系抽取、事件抽取、智能问答等上层应用的底层支撑。如果实体识别这一步做不准后面的下游任务都会跟着崩所以这一层非常重要。1. 项目定位与技术选型思路1.1 这套组合到底解决什么问题BERT-BiLSTM-CRF这个结构已经算是NER领域的“标准答案”了它把三个模块各取所长拼在一起形成一条完整链路。先说结论BERT负责提供上下文相关的语义表征BiLSTM进一步把序列位置信息融合进去CRF层则保证输出的标签序列在全局范围是合法的、合理的。我最早尝试过只用BERT加一个全连接层直接做分类也就是每个token接一个Linear层最后softmax输出每个类别的概率。这种结构实现简单但有一个非常典型的问题模型给出的标签序列可能不合法。举个例子B-PER后面如果直接跟I-PER没问题但B-PER后面如果裸奔一个I-ORG这在实体边界上就是非法组合。全连接层是逐token独立分类的它根本不知道前后标签之间的约束关系。加了CRF层之后模型会学习到一套标签转移规则比如I类标签只能跟在同类型的B标签后面、O后面不能直接跟I标签等等从全局角度把整条序列的联合概率算进去效果提升非常明显。1.2 为什么不单独用BERT或者CRF有人会问既然BERT表征能力这么强直接用BERT加softmax不行吗小样本、领域跨度不大的场景下确实能跑出还不错的分数。但在实体密集、嵌套边界多、标注噪声大的真实数据上没有CRF约束经常会出现标签跳变的问题。而单独用CRF呢它本质上是一个线性统计模型特征工程做起来非常痛苦表达能力远不如深度模型。BiLSTM的加入也有实际意义BERT输出的每个token向量已经融入了上下文但它是Transformer的self-attention机制捕捉长距离依赖很强可是对局部序列的“顺序敏感度”反而没有LSTM那种天然的顺序建模优势。BiLSTM从正反两个方向再把序列过一遍把局部上下文特征进一步强化最终输出给CRF的特征质量更高。三者串起来之后每一层都为下一层准备了更好的输入。用一句话概括BERT打底BiLSTM做局部序列补充CRF做全局最优解各司其职。2. 环境准备PyTorch版本安装与CUDA踩坑记录2.1 Anaconda虚拟环境隔离我强烈建议所有深度学习项目都用Anaconda创建独立的虚拟环境来隔离依赖。不要图省事直接往系统Python里pip install不同项目对PyTorch版本、CUDA版本、transformers版本的依赖经常互相冲突。尤其是有多个项目同时跑的时候环境隔离能救你于水火。我的做法是为这个NER项目单独创建了一个环境conda create -n ner_env python3.9 conda activate ner_envPython版本选3.9是相对稳妥的PyTorch和transformers对3.9的支持非常完善。太新的3.12、3.13版本虽然PyTorch也支持了但个别依赖库比如老版本的apex之类的可能会编译失败没必要冒这个险。2.2 CUDA版本与PyTorch匹配环境建好之后先别急着装PyTorch先确认本机的NVIDIA驱动能支持到哪个CUDA版本。打开命令行执行nvidia-smi看右上角的CUDA Version比如我机器上显示的CUDA Version是12.1那意味着最高可以支持CUDA 12.1的运行时。然后去PyTorch官网选择对应的安装命令。有一个常见的误区是驱动显示的CUDA版本不一定要和你安装的cudatoolkit完全一致驱动版本是向下兼容的只要你的cudatoolkit版本不高于驱动支持的版本就行。如果本机没有独立NVIDIA显卡或者显存不够那就走CPU版本pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu需要GPU加速的话装CUDA版本的命令类似这样pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121注意这里的cu121代表CUDA 12.1必须和你的驱动匹配。装完之后验证一下python -c import torch; print(torch.__version__); print(torch.cuda.is_available())输出True就说明GPU环境OK。这一步如果输出False大概率是驱动和torch版本不匹配或者安装的是CPU版需要卸载重装。2.3 显存不足与依赖包版本管理这个项目的显存占用还是有压力的BERT-base模型本身就一亿多参数加上BiLSTM和CRF之后整体参数量在1.1亿左右。如果显卡显存只有6G或者8Gbatch_size只能设到8甚至4。我自己的显卡跑batch_size16时显存差不多占用到9G。如果你的显存实在吃紧可以考虑两个方向一是改用BERT-small或tiny版本二是开启梯度累积gradient accumulation把batch_size降到4每4个step累积一次梯度再更新参数效果上等效于batch_size16只是训练速度会慢一些。依赖包版本方面这个项目核心依赖是transformers、torch、seqeval、numpy以及pandas。不建议全部用最新版很多最新版之间有兼容性问题。我当前项目里跑的版本组合是transformers 4.36.2、torch 2.1.2、seqeval 1.2.2整体很稳定。如果网络条件不太好的话从pip官方源下载大模型文件可能会超时可以考虑把transformers的模型缓存目录配置好提前下载后面都会用上。2.4 PyTorch 2.6新版本的变化提示如果你安装的是最新版本的PyTorch 2.6及以上有一个非常需要注意的变化torch.load的默认参数weights_only从False改成了True。这个改动的影响是如果你直接用torch.load加载一个包含自定义类实例的checkpoint文件会直接报错提示你要设置weights_onlyFalse。很多人不知道这个变化从2.5升级到2.6之后发现训练好的模型加载不了了。这个我后面会在常见问题部分再详细展开。3. 数据准备与预处理全流程3.1 数据集选型与标注格式我最终选了CLUENER2020作为主实验数据集这是一个公开的中文细粒度命名实体识别数据集包含10个类别地址、书名、公司、游戏、政府、电影、姓名、组织、职位、景点训练集约一万多条样本文本都是来自真实互联网场景的短文本质量不错类别覆盖也比较全面适合验证实体识别模型的泛化能力。原始数据是JSON格式长这样{ text: 彭小军认为国内银行现在走的是台湾的发卡模式所以对风控不是很重视。, label: { name: {mention: [彭小军], offset: [[0, 2]]}, organization: {mention: [台湾], offset: [[14, 15]]} } }我们需要把它转成序列标注模型最常用的BIO格式。B代表实体开始BeginI代表实体内部InsideO代表非实体Outside。同时需要把实体类别编码到标签里去比如人名就是B-PER、I-PER组织机构就是B-ORG、I-ORG。我写了一个转换函数把JSON转成token序列和标签序列def json_to_bio(data_path, save_path): with open(data_path, r, encodingutf-8) as f: lines f.readlines() with open(save_path, w, encodingutf-8) as f: for line in lines: item json.loads(line) text list(item[text]) labels [O] * len(text) for category, entity_info in item[label].items(): mentions entity_info[mention] offsets entity_info[offset] for mention, offset in zip(mentions, offsets): start, end offset[0], offset[1] labels[start] B- category for i in range(start 1, end 1): labels[i] I- category for char, label in zip(text, labels): f.write(char \t label \n) f.write(\n)转换之后每条样本就是两列字符和标签每行一个token空行分隔样本。这种格式是NER任务事实上的标准输入格式绝大多数开源项目都认这种格式。3.2 中文分词与BERT子词对齐这里有一个中文NER特别需要注意的坑BERT的tokenizer对中文的处理方式。bert-base-chinese这个模型实际上是把每个汉字当作一个基本token来切分的所以对于纯中文的句子token和字基本是一一对应的。但实际场景中文本并不全是中文里面可能会混有英文字母、数字、标点遇到这种情况tokenizer会把一个连续的英文单词切分成多个子词subword比如“iPhone”会被切成“i”、“phone”这样。如果我们直接用tokenizer处理后的token序列来对齐标签就会出问题。我的做法是先对原始文本做字符切分再用tokenizer的encode方法拿到offset mapping通过偏移量把BIO标签对齐到tokenizer输出的token序列上。对于[CLS]和[SEP]这两个特殊token标签位置直接给-100这样在计算loss的时候PyTorch的CrossEntropyLoss会自动忽略这些位置的loss。还有一个替代方案是直接遍历原始字符序列对每个字符调用tokenizer的单个字符编码然后按序拼接。但这个方案会丢失英文单词的整体信息所以我最终还是用了offset mapping的方式。3.3 DataLoader构建与mask机制数据准备好之后需要构建PyTorch的Dataset和DataLoader。这里最核心的环节是padding和对齐。因为Batch中的样本长度不同需要padding到同一个长度。padding的时候input_ids用0填充attention_mask用0填充labels用-100填充。这样在计算loss时用交叉熵的ignore_index-100参数padding位置就不会参与梯度计算。完整的数据集类和collate函数长这样class NERDataset(Dataset): def __init__(self, data_path, tokenizer, label2id, max_len128): self.samples [] self.tokenizer tokenizer self.label2id label2id self.max_len max_len self._load_data(data_path) def _load_data(self, data_path): with open(data_path, r, encodingutf-8) as f: lines f.read().split(\n\n) for line in lines: if not line.strip(): continue text [] labels [] for item in line.split(\n): char, label item.split(\t) text.append(char) labels.append(label) self.samples.append((text, labels)) def __len__(self): return len(self.samples) def __getitem__(self, idx): text, labels self.samples[idx] # 使用 offset mapping 对齐 encoding self.tokenizer( text, is_split_into_wordsTrue, return_offsets_mappingTrue, truncationTrue, max_lengthself.max_len ) word_ids encoding.word_ids() aligned_labels [] previous_word_idx None for word_idx in word_ids: if word_idx is None: aligned_labels.append(-100) elif word_idx ! previous_word_idx: aligned_labels.append(self.label2id[labels[word_idx]]) else: aligned_labels.append(self.label2id[labels[word_idx]]) previous_word_idx word_idx return { input_ids: encoding[input_ids], attention_mask: encoding[attention_mask], labels: aligned_labels }这里对word_idx相同的子词都标记为同一个标签是沿用了BERT论文里的做法。但对于中文而言因为基本一个字就是一个token这个分支其实很少触发。在collate函数里对input_ids和labels做padding就好attention_mask本身在tokenizer里面已经处理好了。4. 模型核心实现BERT-BiLSTM-CRF4.1 BERT编码层模型部分先从HuggingFace的transformers库加载预训练的BERT模型。我常用的是bert-base-chinese它一共有12层Transformer编码器隐藏层维度76812个注意力头。加载的代码非常简单from transformers import BertModel, BertTokenizer tokenizer BertTokenizer.from_pretrained(bert-base-chinese) bert BertModel.from_pretrained(bert-base-chinese)这一步会从HuggingFace的模型仓库下载模型权重大概400多M。如果网络不稳定可以设置镜像源import os os.environ[HF_ENDPOINT] https://hf-mirror.com模型加载之后BERT前向计算得到的是所有token的隐层表示。具体来说是取最后一层的输出形状为(batch_size, seq_len, 768)。这里需要强调的是BERT有一个细节最后一个encoder层的输出和hidden_states中最后一层的值是一样的通过output_hidden_statesTrue可以拿到每一层的输出。在NER任务里我们一般直接使用last_hidden_state不需要做特殊拼接因为CRF层对特征的要求是“信息充分即可”不需要像某些语义匹配任务那样去融合多层特征。实际的模型搭建不是直接用BertModel裸奔而是封装成一个PyTorch的nn.Moduleclass BertBiLSTMCRF(nn.Module): def __init__(self, bert_model, num_tags, hidden_size256): super().__init__() self.bert bert_model self.bilstm nn.LSTM( input_sizebert_model.config.hidden_size, hidden_sizehidden_size, num_layers1, bidirectionalTrue, batch_firstTrue ) self.dropout nn.Dropout(0.5) self.fc nn.Linear(hidden_size * 2, num_tags) self.crf CRF(num_tags)这里有一个值得强调的维度问题BiLSTM的hidden_size设成256由于是双向的输出维度就是256*2512然后通过全连接层映射到num_tags也就是标签类别数。4.2 BiLSTM层的作用与实现要点BiLSTM层放在BERT和CRF之间主要作用是进一步提取序列依赖信息。LSTM长短期记忆网络通过门控机制控制信息流动可以很好地模拟文本中的马尔可夫性即当前时刻的状态受前面上下文影响。双向LSTM则是在正向LSTM基础上加了一条反向传播链能够同时利用上文和下文的信息。这里有个细节需要说明为什么需要DropoutBERT输出维度过高768维直接接全连接层非常容易过拟合。加入Dropout层随机丢弃一部分神经元可以让模型不依赖单一维度的特征提升泛化能力。我在项目里dropout概率设为0.5这个值在NER场景下是一个比较稳妥的默认选择。关于BiLSTM的层数我实测下来一层就够了。层数太多参数数量翻倍训练速度明显变慢但F1值几乎没有提升反而有轻微的过拟合风险。如果你的数据量非常大几十万条以上可以考虑两层一般场景下一层够了。4.3 CRF层实现细节CRF层是整个模型中最需要理解清楚的部分。CRF全称条件随机场它的核心思想是在给定观测序列这里是BiLSTM的输出特征的条件下对标签序列建立概率图模型并且通过转移矩阵来建模标签之间的依赖关系。CRF层里有一个可训练的参数叫转移矩阵transition matrix形状是(num_tags, num_tags)。转移矩阵中的第i行第j列表示前一个标签是第i个时当前标签是第j个的得分。这个得分是模型在训练过程中自动学出来的。训练目标是让真实标签序列的得分最大化同时让所有其他可能标签序列的总得分尽可能小。具体实现上CRF有一个前向计算得分的过程使用的是对数求和指数Log-Sum-Exp技巧这也是大家常在代码里看到的函数。我自己实现了一个简化版的CRF类代码核心逻辑如下class CRF(nn.Module): def __init__(self, num_tags): super().__init__() self.num_tags num_tags # 转移矩阵transitions[i][j] 表示从标签i转移到标签j的得分 self.transitions nn.Parameter(torch.randn(num_tags, num_tags)) # 起始和结束得分 self.start_transitions nn.Parameter(torch.randn(num_tags)) self.end_transitions nn.Parameter(torch.randn(num_tags)) def forward_algorithm(self, emissions, mask): # emissions: (batch_size, seq_len, num_tags) batch_size, seq_len, _ emissions.shape score self.start_transitions emissions[:, 0] for t in range(1, seq_len): broadcast_score score.unsqueeze(2) broadcast_emissions emissions[:, t].unsqueeze(1) next_score broadcast_score self.transitions broadcast_emissions next_score torch.logsumexp(next_score, dim1) score torch.where(mask[:, t].unsqueeze(1), next_score, score) score score self.end_transitions return torch.logsumexp(score, dim1) def viterbi_decode(self, emissions, mask): # 维特比解码 batch_size, seq_len, _ emissions.shape score self.start_transitions emissions[:, 0] history [] for t in range(1, seq_len): broadcast_score score.unsqueeze(2) broadcast_emissions emissions[:, t].unsqueeze(1) next_score broadcast_score self.transitions broadcast_emissions best_scores, best_tags next_score.max(dim1) history.append(best_tags) score torch.where(mask[:, t].unsqueeze(1), best_scores, score) score score self.end_transitions best_last_tag score.argmax(dim1) batch_best_tags [] for i in range(batch_size): best_sequence [best_last_tag[i].item()] for h in reversed(history): best_sequence.append(h[i][best_sequence[-1]].item()) best_sequence.reverse() batch_best_tags.append(best_sequence) return batch_best_tags这里需要注意几个点。第一score的初始值是start_transitions加上每个token第一个位置的概率。第二循环遍历序列时score的形状是(batch_size, num_tags)表示“到当前位置为止以每个标签结尾的所有可能序列的得分对数之和”。第三用torch.where操作来处理padding位置让被padding的位置分数保持不变。维特比解码是推断阶段的算法。它和前向算法非常像只不过前向算法用的是logsumexp维特比用的是max。维特比的核心思路是动态规划每一步只保留到当前位置的最佳标签序列及其得分最后通过回溯得到整条最优路径。4.4 整体前向计算与损失函数把三个模块组合起来前向计算分三步走。第一步BERT编码得到文本语义特征第二步BiLSTM进一步处理得到每个位置的标签得分也叫emission scores第三步CRF层将emission scores和转移矩阵结合起来计算整条序列的得分。训练阶段的损失函数是负对数似然Negative Log Likelihood。具体来说真实标签序列的得分是分子所有可能标签序列的得分之和通过前向算法计算是分母两者的比值经过log取负就是损失值。直观理解就是让真实标签序列的得分越高越好让其它所有序列的得分总和越低越好。这里借用一个生活化的类比前向算法像是一家餐厅把所有菜品的销量加起来维特比解码像是找一个最受顾客欢迎的套餐搭配。5. 训练流程与调参实践5.1 参数配置组合模型代码框架搭好之后接下来就是训练环节了。训练参数这块我直接给出实践证明有效的配置参数取值说明batch_size16显存允许的情况下越大越好max_len128超出部分截断过长文本可以分句学习率BERT层2e-5BERT预训练参数需要更小步长学习率下游层1e-3BiLSTM和CRF层可以稍大一些warmup比例0.1前10%的step做学习率预热权重衰减0.01防止过拟合梯度裁剪1.0防止梯度爆炸训练轮数10早停机制配合使用5.2 优化器与学习率调度优化器用的是AdamW它在Adam的基础上修正了权重衰减的实现方式更适合Transformer类的模型。这里有个细节BERT层和下游层BiLSTM、CRF建议设置不同的学习率。因为BERT是预训练模型参数已经收敛得比较好了学习率过大容易把它“摧毁”也就是灾难性遗忘。所以BERT层用2e-5这样的小步长微调下游层用1e-3这样的大步长快速收敛。代码实现上处理分组参数bert_params list(model.bert.parameters()) linear_params list(model.bilstm.parameters()) list(model.crf.parameters()) list(model.fc.parameters()) optimizer torch.optim.AdamW([ {params: bert_params, lr: 2e-5}, {params: linear_params, lr: 1e-3} ], weight_decay0.01)学习率调度器用了一个带warmup的线性衰减。为什么需要warmup因为模型在初始阶段参数是随机初始化的如果一开始就用较大的学习率容易导致训练不稳定出现loss突然变大的情况。warmup让学习率从0开始线性增长到预设值让模型先热身然后再线性衰减到0。from transformers import get_linear_schedule_with_warmup total_steps len(train_loader) * epochs scheduler get_linear_schedule_with_warmup( optimizer, num_warmup_stepsint(total_steps * 0.1), num_training_stepstotal_steps )5.3 评估指标与实体级别F1训练过程中不能只看loss还要关注实体级别的精确率、召回率和F1分数。这里有一个很多新手会踩的坑千万别直接用sklearn的classification_report去算token级别的准确率因为它把每个token当作独立样本没有考虑实体边界的整体性。比如一个实体有5个token你预测对了4个tokentoken级别可能给了你80%的准确率但从实体角度看这整个实体就是预测错了因为边界没对齐。正确的做法是使用seqeval库它计算的是实体级别的指标from seqeval.metrics import classification_report from seqeval.scheme import IOB2 true_labels [] # 真实标签序列列表 pred_labels [] # 预测标签序列列表 report classification_report( true_labels, pred_labels, modestrict, schemeIOB2 ) print(report)strict模式保证实体的类型、边界完全一致才算预测正确这才是真正反映业务效果的指标。我印象很深刻第一次用token级别评估F1看起来有92%换成实体级别之后直接掉到82%差距非常大。所以做NER项目一定以实体级别的F1为准。模型保存的时候因为整个模型包含了BERT、BiLSTM、CRF结构比较复杂我建议保存整个checkpoint而不只是state_dicttorch.save({ model_state_dict: model.state_dict(), label2id: label2id, id2label: id2label, config: model.config }, best_model.pt)6. 常见问题与排查技巧实录6.1 训练不收敛或者Loss为NaN训练过程中遇到loss为NaN是最让人头疼的。这个问题我在项目里遇到过两次最后定位到的主要原因有两个一是学习率太大BERT层还好但CRF层如果学习率设成1e-2这种偏大的值很容易让转移矩阵的数值过大导致前向算法里的logsumexp溢出。二是标签对齐出了问题如果labels里混入了超出num_tags范围的数值比如原始类别数对不上计算loss时就会出现异常。排查步骤如下第一步检查labels的取值确实是0到num_tags-1之间不能出现num_tags本身第二步把学习率调小一个数量级测试第三步打印前向计算的中间结果确认emissions没有出现NaN值。还有一个很容易被忽略的点如果用了混合精度训练AMP偶尔会因为梯度裁剪参数设置不当导致数值不稳定可以先关闭AMP测试。6.2 PyTorch 2.6版本的weights_only问题这个坑比较新很多人升级PyTorch到2.6之后发现之前保存的模型加载不了了报错信息类似于“Weights only load failed, please set weights_onlyFalse”。这是因为PyTorch 2.6把torch.load的默认参数weights_only改为了True导致加载包含自定义类实例的checkpoint时被拒绝。解决办法有三种第一种在torch.load时显式设置weights_onlyFalse第二种把模型参数单独用model.state_dict()保存加载的时候传weights_onlyTrue默认值就行第三种采用safetensors格式保存模型权重它的加载速度和安全性都好于torch.load。我个人建议优先用safetensors它不需要依赖pickle安全性更高加载速度也有提升transformers新版本已经默认支持了。6.3 标签对齐出错导致模型效果差刚才提到过如果文本中混有英文单词tokenizer会把一个英文单词切成多个子词导致token数量和字符数量不一致。如果直接按字符序号去对齐标签轻则错位重则在后续计算loss时因维度不匹配直接报错。这个问题排查起来有点隐蔽因为有时候模型不报错只是F1值特别低。我个人总结了两个自查技巧第一随机挑几条样本手动打印出input_ids对应的token以及对齐后的labels肉眼检查是否一一对应第二统计一下训练集中label-100的比例如果过高比如超过50%说明padding太多了可以适当调低max_len减少无效计算。还有一种情况是tokenizer对特殊字符比如全角空格、换行符的处理和原始文本不一致容易导致offset mapping错位最终对齐失败。解决方案是对原始文本先做一次清洗把特殊字符统一转成空格再送入tokenizer。6.4 推理阶段出现非法标签组合即使训练时CRF约束得很好推理阶段还是偶尔会出现比如B-PER后面直接跟I-ORG这种非法标签组合。根本原因是序列太长的时候维特比解码的得分差异可能非常小模型在置信度判断上出现了模糊。如果真的出现这种问题可以在预测之后加一个后处理规则遍历预测标签序列如果遇到I类型的标签前面不是同类B或同类I就把I改成O。这个规则虽然粗糙但能有效兜底保证输出标签始终是合法的。6.5 数据量太小或领域差异大怎么办如果你的使用场景和BERT预训练语料差异很大比如医疗病历、法律文书、特种设备日志直接用bert-base-chinese跑可能效果不理想。一个非常有效的改进方向是做领域自适应预训练也就是用大量领域无标注语料对BERT做一次MLM继续训练然后再用少量标注数据做NER微调。这个方案在实践中提升非常明显我在简历场景下直接微调F1大概在88.5%加上领域预训练之后提升到了91.2%。如果手上没有大量无标注数据也可以用RoBERTa-wwm-ext全词掩码中文预训练模型替代原版BERT中文场景下通常会有1到2个百分点的提升。7. 项目完整效果与一些个人的实在话最终在CLUENER2020测试集上这个BERT-BiLSTM-CRF基线跑出来的实体级F1分数大概在79%到81%之间。换到简历数据集上由于实体类型比较集中人名、学校、公司、职位F1可以到90%左右。不同数据集的指标差异非常大不要用一个固定的分数去衡量模型好坏关键是要看它在你自己的业务数据上是否够用。这个项目前前后后跑了差不多三周从最初规则系统到最后的深度学习方案我最大的体会是不要迷信某一个技术组件BERT-BiLSTM-CRF能成为经典方案不是因为某个模块有多强而是这套组合把序列建模和标签约束都照顾到了。数据质量永远是第一位的同样的模型标注规范的数据和混乱标注的数据F1能差出十个百分点以上。如果你正打算复现这个系统建议先把数据格式化搞明白再研究模型细节。环境搭配建议直接用我上文给出的版本组合能省很多不必要的折腾。本文还有配套的精品资源点击获取
返回列表