
简介命名实体识别NER是自然语言处理NLP中的一项基础且关键的任务旨在从非结构化文本中自动识别并分类具有特定意义的实体如人名、地名和组织机构名。其核心原理是将文本序列转换为标签序列属于序列标注问题。传统方法依赖规则和词典而现代主流技术则基于统计机器学习与深度学习模型通过捕捉上下文语义和标签间依赖关系来提升准确性与泛化能力。在众多模型中BiLSTM-CRF架构因其在特征提取与全局解码上的优异平衡成为深度学习时代序列标注的经典范式具有重要的教学与实践价值。该模型结合了双向长短时记忆网络BiLSTM的上下文编码能力和条件随机场CRF的标签转移约束广泛应用于信息抽取、智能问答和知识图谱构建等场景。本文聚焦于BiLSTM-CRF的核心架构与实战调优深入拆解其从词向量表示、特征提取到CRF解码的完整流程并分享在中文NER任务中针对模型收敛、过拟合、性能提升等常见问题的工程解决方案与调参心得。1. 项目概述从序列标注到实体识别在自然语言处理NLP的众多任务中命名实体识别NER绝对算得上是“基本功”级别的存在。简单来说它的任务就是从一段文本里把那些具有特定意义的实体给揪出来比如人名、地名、组织机构名、时间、货币等等。这活儿听起来简单但做起来可不容易因为自然语言充满了歧义和复杂性。比如“苹果”这个词在“我吃了一个苹果”里是水果在“苹果公司发布了新产品”里就是公司名机器怎么知道该标哪个早期的方法主要靠规则和词典比如写一堆正则表达式去匹配或者维护一个庞大的实体词库。这种方法在特定领域、封闭场景下还能用用但面对开放域、新词频出的真实文本就显得力不从心了维护成本高泛化能力差。后来统计机器学习方法比如隐马尔可夫模型HMM、条件随机场CRF开始成为主流它们能从标注好的数据里自动学习规律效果和泛化性都上了一个台阶。而近年来随着深度学习的爆发基于神经网络的NER模型彻底改变了这个领域的游戏规则。其中“BiLSTM-CRF”这个组合可以说是深度学习时代命名实体识别的一个经典“范式”甚至可以说是很多从业者入门序列标注任务的“第一课”。它巧妙地将能够捕捉长距离上下文信息的双向长短时记忆网络BiLSTM和擅长对标签间依赖关系进行全局建模的条件随机场CRF结合了起来在多个公开数据集上都取得了当时非常出色的效果。即便现在Transformer比如BERT大行其道理解BiLSTM-CRF的架构思想和实现细节对于深入理解序列标注任务、对比模型演进乃至在某些资源受限或对推理速度有要求的场景下进行模型选型都有着不可替代的价值。这篇文章我就结合自己多次复现和调优这个模型的经验把它从里到外拆解一遍不仅告诉你它怎么工作更会分享在实操中那些容易踩坑的细节和调参心得。2. 核心架构与原理深度拆解要搞懂BiLSTM-CRF我们不能把它当成一个黑盒而是得拆开看明白每个部件的作用以及它们是如何协同工作的。整个模型的处理流程可以概括为输入文本经过嵌入层Embedding变成向量序列这个序列送入BiLSTM层捕获上下文特征输出每个位置的标签分数最后CRF层利用转移矩阵对这些分数进行全局调整输出最合理的标签序列。2.1 基石词向量与序列表示任何神经网络模型处理文本的第一步都是把离散的符号字或词转换成连续的数值向量也就是词向量。这一步的质量直接影响了模型能吸收多少语义信息。为什么需要词向量计算机无法直接理解“北京”和“上海”都是城市但如果我们通过大量语料训练使得“北京”和“上海”的词向量在空间中的距离很近而和“跑步”的词向量距离很远那么模型就间接“学会”了它们属于同一类别。常用的方式有静态预训练词向量如Word2Vec、GloVe。在训练NER模型前先用大规模无标注语料训练好一个固定的词表每个词对应一个固定向量。优点是训练快词向量质量较高缺点是无法解决一词多义问题“苹果”永远只有一个向量且无法处理未登录词OOV。动态上下文词向量如ELMo或者直接使用BERT等模型的输出。每个词的向量会根据它所在的句子上下文动态变化完美解决了一词多义。这也是当前的主流。但在经典的BiLSTM-CRF实现中我们通常从静态词向量开始因为它结构清晰便于理解核心机制。实操心得对于中文NER我强烈建议使用“字向量”而非“词向量”作为基本输入单元。原因有三第一可以彻底规避分词错误带来的误差传播分词错了实体边界肯定错第二能更好地处理未登录词和网络新词第三中文字本身包含部分语义如“江”、“河”多与水有关字向量模型通常能取得比词向量模型更好或相当的效果。我们可以用预训练的中文字向量比如用中文维基百科或新闻语料训练的Word2Vec字模型进行初始化。假设我们有一个句子“李明访问了北京大学”分词后是[“李明” “访问” “了” “北京大学”]。在字模型中我们将其处理为字符序列[“李” “明” “访” “问” “了” “北” “京” “大” “学”]。每个字通过查找嵌入矩阵被转换为一个固定维度如100维的向量。于是一个长度为L的句子就被表示为一个L x 100的矩阵这就是BiLSTM的输入。2.2 核心特征提取器双向LSTM详解拿到序列的向量表示后接下来就需要一个强大的特征提取器来理解上下文。循环神经网络RNN本是处理序列数据的天然选择但普通RNN有严重的梯度消失/爆炸问题难以学习长距离依赖。长短时记忆网络LSTM通过引入门控机制较好地解决了这个问题。LSTM单元在NER中的关键作用对于一个位置的字如“京”LSTM单元会综合当前字的向量输入和上一个位置的隐藏状态计算出一个新的隐藏状态。这个隐藏状态可以被认为是模型读到当前位置时对之前所有信息的一个“总结”。但这里有个问题它只总结了“过去”的信息从左到右。在实体识别中“未来”的上下文同样关键。比如判断“京”是不是地名的一部分看到后面的“大学”二字把握就大得多。因此双向LSTMBiLSTM成为了标准配置。它的思想很简单但极其有效部署两个独立的LSTM层一个按句子正序前向读取得到每个位置基于上文信息的隐藏状态h_f。另一个按句子逆序后向读取得到每个位置基于下文信息的隐藏状态h_b。最后将每个位置的前向和后向隐藏状态拼接起来[h_f; h_b]作为该位置的最终上下文表征。这样对于序列中的每一个字它的向量表示都融合了从左到右和从右到左的全部上下文信息。这个拼接后的向量蕴含了判断该字属于何种实体或非实体的丰富特征。注意事项BiLSTM的输出维度是隐藏层维度的两倍因为拼接。如果你的单向LSTM隐藏层维度设为100那么BiLSTM的输出就是200维。在设计后续的全连接层时输入维度需要与此匹配。2.3 序列解码的约束大师条件随机场CRF如果只有BiLSTM我们通常会在其输出后接一个全连接层Softmax为每个位置独立地预测一个标签。这被称为“点式”分类。但这种方法有一个明显缺陷它忽略了标签之间的依赖关系。例如在经典的BIOBegin, Inside, Outside标注体系下“I-PER”人名内部前面不可能紧跟着“I-ORG”组织机构内部它前面大概率应该是“B-PER”或“I-PER”。“O”非实体后面可以接任何标签但“B-LOC”地名开始后面接“O”的概率就非常低。点式分类无法建模这种标签间的转移规律可能导致预测出[O, I-PER]这样不合法的序列。而条件随机场CRF层正是为了解决这个问题而引入的。它不是单独看待每个位置的标签而是对整个标签序列的联合概率进行建模。CRF层的工作原理发射分数Emission Score这部分由BiLSTM提供。BiLSTM层后面的全连接层会为每个位置i的每个可能标签t输出一个分数记为P_i(t)。这个分数可以理解为“不考虑上下文标签当前位置是标签t的可能性有多大”。转移分数Transition Score这是CRF层自己学习的一个矩阵记为T。矩阵的大小是[标签数量, 标签数量]。其中T[s, t]表示从标签s转移到标签t的分数。这个分数可以理解为“前一个位置是标签s的情况下下一个位置是标签t的合理性有多大”。通过训练模型会学到“B-PER - I-PER”的分数很高而“I-PER - B-LOC”的分数很低。序列总分对于一个给定的输入句子X和一个候选标签序列y [y1, y2, ..., yn]CRF模型为其打出的总分是S(X, y) sum(发射分数_i) sum(转移分数_{i-1, i})。即所有位置的发射分数之和加上所有相邻标签对之间的转移分数之和。训练与解码训练目标是最大化正确标签序列的分数相对于所有可能序列分数的概率使用Softmax over所有序列。这通过负对数似然损失来实现。在反向传播时不仅BiLSTM的参数会被更新CRF的转移矩阵T也会被一起优化。解码预测在预测时我们需要找到使得总分S(X, y)最大的那个标签序列y*。这是一个寻找最优路径的问题可以使用高效的维特比算法Viterbi Algorithm在O(n * k^2)的时间内解决其中n是序列长度k是标签数量。CRF与BiLSTM的结合实现了“112”的效果BiLSTM利用其强大的上下文建模能力提供了高质量的、考虑语境的“发射分数”CRF则利用其全局序列建模能力引入了标签间的语法约束对BiLSTM的原始输出进行“纠偏”和“平滑”最终输出一个全局最优的、符合标签语法规则的实体序列。3. 从零到一的完整实现流程理解了原理我们动手实现一个中文命名实体识别模型。这里我将以PyTorch框架为例因为它的动态图特性让模型构建和调试非常直观。我们会使用“字”作为基本单元标签体系采用最常用的BIOES比BIO更精细E表示实体结束S表示单字实体。3.1 数据准备与预处理任何NLP项目都始于数据。我们假设你已经有了一个标注好的数据集格式可能是每行“字\t标签”。第一步构建词汇表和标签表这是将文本和标签数值化的基础。我们需要遍历所有训练数据。def build_vocab_label(data_path): chars set() labels set() with open(data_path, r, encodingutf-8) as f: for line in f: line line.strip() if line: char, label line.split(\t) chars.add(char) labels.add(label) # 添加特殊符号 char2id {PAD: 0, UNK: 1} # 按频率排序后添加这里简化为直接添加 for idx, char in enumerate(chars, startlen(char2id)): char2id[char] idx label2id {} for idx, label in enumerate(labels): label2id[label] idx # 通常需要固定BOS/EOS标签用于CRF这里假设标签表中已包含 return char2id, label2id, id2char, id2label注意事项PAD用于将批次内不等长的句子补齐到相同长度UNK用于处理测试集中未在训练集出现过的字。标签表中需要确认是否包含了CRF层需要的开始START和结束END标签它们不参与实际标注但用于转移矩阵的计算。第二步创建数据加载器DataLoader我们需要将数据组织成(input_ids, label_ids, sequence_lengths)的元组并支持批量处理。from torch.utils.data import Dataset, DataLoader class NERDataset(Dataset): def __init__(self, data_path, char2id, label2id, max_len150): self.samples [] with open(data_path, r, encodingutf-8) as f: lines f.readlines() sentence, tags [], [] for line in lines: line line.strip() if not line: # 空行表示句子结束 if sentence: # 截断或填充 sent_ids [char2id.get(c, char2id[UNK]) for c in sentence[:max_len]] tag_ids [label2id.get(t) for t in tags[:max_len]] # 填充 pad_len max_len - len(sent_ids) sent_ids [char2id[PAD]] * pad_len tag_ids [label2id[O]] * pad_len # 通常用O标签填充 self.samples.append((sent_ids, tag_ids, len(sentence))) sentence, tags [], [] else: char, tag line.split(\t) sentence.append(char) tags.append(tag) # 处理最后一个句子 if sentence: ... # 同上 def __len__(self): return len(self.samples) def __getitem__(self, idx): return self.samples[idx] def collate_fn(batch): # batch是一个列表每个元素是(sent_ids, tag_ids, length) sent_ids, tag_ids, lengths zip(*batch) # 转换为LongTensor sent_ids torch.LongTensor(sent_ids) tag_ids torch.LongTensor(tag_ids) lengths torch.LongTensor(lengths) return sent_ids, tag_ids, lengths创建DataLoadertrain_dataset NERDataset(train.txt, char2id, label2id) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, collate_fncollate_fn)3.2 模型构建定义BiLSTM-CRF网络现在我们来搭建模型的核心。我们将使用torch.nn.Module来定义网络并使用torchcrf这个第三方库来实现CRF层当然你也可以自己实现维特比算法但使用库更稳定高效。import torch import torch.nn as nn from torchcrf import CRF class BiLSTM_CRF(nn.Module): def __init__(self, vocab_size, tagset_size, embedding_dim100, hidden_dim256): super(BiLSTM_CRF, self).__init__() self.embedding_dim embedding_dim self.hidden_dim hidden_dim self.vocab_size vocab_size self.tagset_size tagset_size # 1. 嵌入层 self.embedding nn.Embedding(vocab_size, embedding_dim, padding_idx0) # 0对应PAD # 2. BiLSTM层 self.lstm nn.LSTM(embedding_dim, hidden_dim // 2, num_layers2, # 使用2层LSTM以增强表达能力 bidirectionalTrue, batch_firstTrue, dropout0.5) # 层间Dropout防止过拟合 # 3. 将BiLSTM输出映射到标签空间的全连接层 self.hidden2tag nn.Linear(hidden_dim, tagset_size) # 4. CRF层 self.crf CRF(tagset_size, batch_firstTrue) def forward(self, sentence, tagsNone, maskNone): sentence: [batch_size, seq_len] tags: [batch_size, seq_len] (训练时提供) mask: [batch_size, seq_len] (BoolTensor, True for valid token) # 获取掩码非填充位置 if mask is None: mask sentence ! 0 # 假设padding_idx0 embeds self.embedding(sentence) # [batch, seq_len, emb_dim] # LSTM需要打包序列以处理变长 lengths mask.sum(dim1).cpu() packed_embeds nn.utils.rnn.pack_padded_sequence(embeds, lengths, batch_firstTrue, enforce_sortedFalse) lstm_out, _ self.lstm(packed_embeds) lstm_out, _ nn.utils.rnn.pad_packed_sequence(lstm_out, batch_firstTrue) # [batch, seq_len, hidden_dim] # 计算发射分数 emissions self.hidden2tag(lstm_out) # [batch, seq_len, tagset_size] # CRF计算 if tags is not None: # 训练模式计算负对数似然损失 loss -self.crf(emissions, tags, maskmask, reductionmean) return loss else: # 预测模式解码得到最优路径 best_paths self.crf.decode(emissions, maskmask) return best_paths关键参数解析embedding_dim字向量的维度。通常预训练词向量是多少维就用多少维如100, 200, 300。如果从头训练100-300是常见范围。hidden_dimLSTM隐藏状态的维度。BiLSTM的输出维度是hidden_dim因为bidirectionalTrue实际每个方向是hidden_dim//2。这是一个关键超参数太小则模型容量不足太大会过拟合且计算慢。256-512是常用的起点。num_layersLSTM的层数。堆叠多层可以增加模型深度捕捉更复杂的特征但也会增加训练难度和过拟合风险。通常1-3层配合Dropout使用。dropoutLSTM层间的Dropout率是防止过拟合的利器。0.3-0.5是常见值。3.3 模型训练与评估构建好模型和数据管道后就可以开始训练了。训练循环import torch.optim as optim from seqeval.metrics import classification_report, f1_score # 用于序列标注评估的库 device torch.device(cuda if torch.cuda.is_available() else cpu) model BiLSTM_CRF(len(char2id), len(label2id)).to(device) optimizer optim.Adam(model.parameters(), lr0.001, weight_decay1e-4) # 加入权重衰减 scheduler optim.lr_scheduler.ReduceLROnPlateau(optimizer, modemax, factor0.5, patience3) # 基于F1值调整学习率 num_epochs 50 best_f1 0.0 for epoch in range(num_epochs): model.train() total_loss 0 for batch_idx, (sent, tags, lengths) in enumerate(train_loader): sent, tags sent.to(device), tags.to(device) mask sent ! 0 optimizer.zero_grad() loss model(sent, tags, mask) loss.backward() # 梯度裁剪防止梯度爆炸这对RNN类模型很重要 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0) optimizer.step() total_loss loss.item() avg_train_loss total_loss / len(train_loader) print(fEpoch {epoch1}, Train Loss: {avg_train_loss:.4f}) # 在验证集上评估 model.eval() all_preds, all_labels [], [] with torch.no_grad(): for sent, tags, lengths in dev_loader: # 假设有开发集dev_loader sent, tags sent.to(device), tags.to(device) mask sent ! 0 preds model(sent, maskmask) # 得到解码后的标签序列列表 # 将id转换回标签并去除padding部分 for i, length in enumerate(lengths): pred_label_seq [id2label[p] for p in preds[i][:length]] true_label_seq [id2label[t.item()] for t in tags[i][:length]] all_preds.append(pred_label_seq) all_labels.append(true_label_seq) # 使用seqeval评估它考虑实体级别 f1 f1_score(all_labels, all_preds, averagemacro) # 或micro print(fValidation F1: {f1:.4f}) print(classification_report(all_labels, all_preds)) # 学习率调整和模型保存 scheduler.step(f1) if f1 best_f1: best_f1 f1 torch.save(model.state_dict(), best_model.pth) print(fBest model saved with F1: {best_f1:.4f})训练技巧优化器选择Adam是默认且通常有效的选择。学习率从1e-3开始尝试。梯度裁剪对于RNN/LSTM梯度裁剪至关重要可以稳定训练过程。学习率调度使用ReduceLROnPlateau在指标停滞时降低学习率有助于模型收敛到更优解。评估指标不要使用简单的准确率Accuracy因为句子中大部分标签是“O”准确率会虚高。必须使用实体级别的F1值seqeval库是标准工具。早停如果验证集F1在连续多个epoch如10个不再提升就停止训练防止过拟合。4. 实战调优与性能提升策略一个基础的BiLSTM-CRF模型跑起来后我们往往会发现效果离SOTA还有距离。别急下面这些调优策略能帮你把模型性能提升一个档次。4.1 嵌入层的艺术静态、动态与混合我们之前提到使用静态预训练字向量。但我们可以做得更好。策略一Fine-tuning嵌入层在训练时不冻结嵌入层的参数。让模型根据NER任务微调字向量使向量空间更贴合当前任务。这通常会带来显著提升尤其是当预训练语料和你的任务领域差异较大时。策略二多通道嵌入CharWord对于中文可以同时使用字向量和词向量。将句子分词后每个词也转换为词向量然后通过一个简单的CNN或RNN来得到词的字符级表示最后将字向量和词表示拼接或相加。这能同时利用字和词的信息但实现更复杂。策略三使用预训练语言模型LM特征这是当前的主流和最强方法。我们可以将BERT等模型的某一层通常是最后几层的输出作为字的上下文向量直接输入给BiLSTM或者干脆用BERTCRF。这能极大提升性能尤其是对于歧义和未登录词。在经典的BiLSTM-CRF框架下我们可以用BERT的输出来初始化嵌入层或者将其作为额外的特征通道与静态字向量拼接。# 示例使用BERT获取上下文向量简化版 from transformers import BertModel, BertTokenizer bert_model BertModel.from_pretrained(bert-base-chinese) tokenizer BertTokenizer.from_pretrained(bert-base-chinese) def get_bert_features(sentence_chars): # sentence_chars: 字列表如 [李, 明, 访, 问, ...] # BERT需要分词中文BERT通常以字为单位 tokens [[CLS]] sentence_chars [[SEP]] input_ids tokenizer.convert_tokens_to_ids(tokens) with torch.no_grad(): outputs bert_model(torch.tensor([input_ids])) last_hidden_states outputs.last_hidden_state # [1, seq_len, 768] # 去掉[CLS]和[SEP]对应的向量与原始字序列对齐 word_embeddings last_hidden_states[0, 1:-1, :] return word_embeddings # [seq_len, 768] # 在模型中可以将bert_features与常规embedding拼接 # combined torch.cat([char_embeds, bert_features], dim-1)4.2 网络结构优化超越朴素BiLSTM堆叠与残差尝试使用更深的BiLSTM如3-4层并在层之间添加残差连接有助于训练更深的网络捕捉更复杂的模式。ID-CNN替代LSTM膨胀卷积神经网络ID-CNN在保持感受野的同时拥有比RNN更高的并行计算效率推理速度更快。对于长文本序列ID-CNN是LSTM的一个有力替代品。注意力机制在BiLSTM之上添加自注意力Self-Attention或注意力层让模型能够动态地关注序列中更重要的部分。例如在判断一个词是否是实体时注意力机制可以告诉模型应该更关注它周围的哪些词。4.3 损失函数与标签平滑标准的损失函数是CRF的负对数似然损失。但我们还可以引入一些技巧Focal Loss如果数据集中实体标签B/I和非实体标签O极度不平衡O标签占大多数标准交叉熵损失会被O标签主导。Focal Loss通过降低易分类样本如大量的O的权重让模型更关注难分类的实体样本。标签平滑Label Smoothing在计算发射分数时对真实的one-hot标签进行平滑处理如0.9的概率给真实标签0.1的概率均匀分给其他标签可以防止模型对训练数据过度自信起到正则化作用可能提升泛化能力。4.4 解码策略与后处理即使有了CRF预测结果也可能不完美。一些后处理规则可以作为补充强制约束根据业务逻辑添加一些硬性规则。例如在医疗文本中某个特定词永远只可能是某种疾病实体。可以在解码后用规则覆盖模型的预测。词典匹配如果拥有高质量的实体词典可以将词典匹配的结果与模型预测结果进行融合。例如对于词典中存在的实体如果模型预测的置信度较低可以优先采用词典结果。阈值过滤CRF解码给出的是最优路径但我们可以通过计算该路径的概率或对比次优路径的概率差得到一个置信度。对于低置信度的预测实体可以选择不输出或交由人工复核。5. 常见问题排查与避坑指南在实际操作中你肯定会遇到各种各样的问题。这里我总结了一些典型“坑位”及其解决方案。5.1 模型不收敛或Loss为NaN这是最令人头疼的问题之一。检查数据首先确认数据中是否有异常字符、标签错误或空样本。确保char2id和label2id映射正确没有出现-1之类的非法索引。梯度爆炸这是导致NaN的常见原因。务必进行梯度裁剪clip_grad_norm_。将max_norm设置在1.0到5.0之间尝试。学习率过高尝试大幅降低学习率例如从1e-3降到1e-4甚至1e-5。使用学习率预热Warmup策略也是一个好办法。嵌入层初始化如果使用随机初始化的嵌入层检查初始化范围是否合理。可以尝试使用Xavier或Kaiming初始化。损失函数检查CRF层的输入发射分数是否有极值。可以尝试在self.hidden2tag后加入LayerNorm或tanh进行激活将分数范围限制在一定区间。5.2 模型过拟合严重训练集Loss一直降验证集F1值早早就停止增长甚至下降。增加正则化Dropout这是最有效的武器。在LSTM层之间、全连接层之后都可以加。尝试将dropout率从0.3提高到0.5。权重衰减L2正则在优化器中设置weight_decay参数如1e-4。早停严格监控验证集F1一旦连续多个epoch不提升就停止。数据增强对于NER简单的数据增强比较困难但可以尝试同义词替换用同义词替换句子中的非实体词。实体替换在保持句子语法通顺的前提下用同类型的其他实体替换原有实体如将“张三访问北京”改为“李四访问上海”。这需要有一个实体库。简化模型如果数据量不大尝试减少hidden_dim或num_layers降低模型容量。5.3 预测速度慢BiLSTM的序列特性导致其无法像CNN那样完全并行预测长文本时可能较慢。批量预测确保在预测时也使用DataLoader进行批处理能充分利用GPU并行能力。使用ID-CNN如前所述考虑用ID-CNN替换LSTM能获得显著的推理加速。模型剪枝与量化训练完成后可以对模型进行剪枝移除不重要的连接和量化将FP32参数转换为INT8在不显著损失精度的情况下大幅减小模型体积和提升推理速度。PyTorch提供了相关的工具。使用更小的模型如果业务允许尝试更小的embedding_dim和hidden_dim。5.4 特定类别实体识别效果差模型整体F1不错但某个特定类型如“组织机构”的识别召回率很低。样本不平衡检查训练数据中该类实体的数量是否远少于其他类。如果是可以对该类样本进行过采样复制。在损失函数中为该类设置更高的权重class weight。使用Focal Loss。特征不足该类实体可能具有独特的上下文模式或内部构词规律而模型没有学到。可以尝试引入额外的特征如该词是否包含特定后缀如“公司”、“局”对于组织机构或者使用预训练的语言模型来提供更丰富的上下文语义。针对该类实体设计一些简单的后处理规则进行补充。5.5 标签体系选择BIO、BIOES还是BILOU这是一个实践中的常见选择。BIO最简单只有B开始、I内部、O外部。但对于长实体无法区分中间和结尾。BIOES增加了E结束和S单个实体信息更丰富能明确指示实体边界通常能带来1-2个百分点的F1提升是当前最主流的选择。BILOU与BIOES类似用L最后代替E用U单元代替S。效果与BIOES相差无几。个人建议除非有历史包袱必须用BIO否则新项目一律推荐使用BIOES。它提供了更精确的边界信息对CRF学习转移规律更有帮助。转换脚本也很容易写。最后我想说的是BiLSTM-CRF作为一个经典的序列标注模型其价值不仅在于其本身的效果更在于它为我们理解序列建模、标签依赖和神经网络与概率图模型的结合提供了一个绝佳的范本。即便你现在直接使用BERT来做NER了解BiLSTM-CRF的运作机制也能让你更深刻地理解BERT的CRF输出头在做什么以及为什么需要它。在实际工业场景中当计算资源受限、或需要极低延迟时一个精心调优的BiLSTM-CRF模型仍然是极具竞争力的选择。我的经验是在中等规模的数据集上结合好的预训练字向量和充分的调优BiLSTM-CRF达到与轻量级BERT模型如ALBERT-Tiny相近的性能是完全可能的而它的推理速度却要快上一个数量级。本文还有配套的精品资源点击获取