声明:博客未经允许禁止抄袭转载。
前言
最近有粉丝在后台私信我能不能更一篇关于命名实体识别(NER,Named Entity Recognition)的经典模型BiLSTM-CRF的实战文章,前段时间有点忙所有一直没有更新,趁着最近有点空,满足一下这个粉丝的愿望,话不多说直接上干货。
说明:为方便起见,本文沿用了之前的博客NLP实战:面向中文电子病历的命名实体识别中的数据集。关于命名实体识别的概念、实验评价指标、数据集介绍以及数据预处理的详细信息,同样可以参考我上面发布的博客。
词嵌入
关于词嵌入,本文的重点是探索条件随机场(Conditional Random Field, CRF)对命名实体识别的影响,因此弱化了词嵌入,直接采用了可学习的词嵌入nn.Embedding
。为此,需要在预处理的过程中在训练数据集语料上构建字符到数字的映射词典,其中每个数字都对应词嵌入矩阵中一个可学习词嵌入的索引。
def get_dict(data, filter_word_num):
# 统计词频
word_count = {}
for sample in data:
text = sample.get('originalText')
for word in text:
word_count[word] = word_count.get(word, 0) + 1
# 过滤低频词
word2id = {
"PAD": 0,
"UNK": 1
}
for word, count in word_count.items():
if count >= filter_word_num:
word2id[word] = len(word2id)
print("Total %d tokens, filter count<%d tokens, save %d tokens."%(len(word_count)+2, filter_word_num, len(word2id)))
with open("processed/word2id.json", "w", encoding="utf-8") as fp:
json.dump(word2id, fp, ensure_ascii=False)
return word2id
模型设计与实现
BiLSTM-CRF是NER任务的经典模型(模型架构图如下),该模型利用双向LSTM从正向和逆向来更好的捕获语料序列的上下文关系,然后利用CRF来添加规则约束,避免许多不合理的预测,从而使得预测更加准确。
条件随机场
在早期,NER通常直接使用循环神经网络来对序列进行编码,然后利用MLP来独立地预测各个token属于各个类别的概率。这种方式并没有考虑序列级别的相关性。例如对于某预测序列的当前词,若其正确标签为B-Disease&Dianonsis
,那么当前词的下一个词的正确标签极大概率为I-Disease&Dianonsis
,而不可能是B-Inspection
等等。而引入CRF便可以从序列级别来添加类似这种的规则约束,从而提升分类准确率。
令
x
=
{
x
1
,
x
2
,
.
.
.
,
x
T
}
\mathbf{x}=\{x_1, x_2, ..., x_T\}
x={x1,x2,...,xT}表示输入序列,
y
=
{
y
1
,
y
2
,
.
.
.
y
T
}
\mathbf{y}=\{y_1,y_2,...y_T\}
y={y1,y2,...yT}表示输入序列对应的真实标签序列,
P
∈
R
T
×
n
P\in\mathbb{R}^{T\times n}
P∈RT×n表示将
x
\mathbf{x}
x经过BiLSTM编码后经MLP分类的预测概率矩阵,
P
i
,
j
P_{i,j}
Pi,j表示将序列的第
i
i
i个词预测为类别
j
j
j的概率,其中
n
n
n表示类别数,
T
T
T表示序列的长度。对于CRF而言,它需要学习一个转移概率矩阵
A
∈
R
n
×
n
A \in \mathbb{R}^{n \times n}
A∈Rn×n,
A
i
,
j
A_{i,j}
Ai,j表示若当前词预测类别为
i
i
i,下一个词预测预测为类别
j
j
j的概率。CRF会对整个序列的预测结果进行打分,以输入序列与真实标签序列为例,其得分
S
(
x
,
y
)
S(\mathbf{x},\mathbf{y})
S(x,y)的计算公式为:
S
(
x
,
y
)
=
∑
i
=
0
T
A
y
i
,
y
i
+
1
+
∑
i
=
1
T
P
i
,
y
i
S(\mathbf{x}, \mathbf{y})=\sum_{i=0}^T A_{y_i, y_{i+1}}+\sum_{i=1}^T P_{i, y_i}
S(x,y)=i=0∑TAyi,yi+1+i=1∑TPi,yi
上述计算公式不仅考虑了每个词的概率,还考虑了词与词之间的转移概率。对于CRF,其优化目标便是最大化
S
(
x
,
y
)
S(\mathbf{x}, \mathbf{y})
S(x,y)在所有可能出现的预测序列中的概率:
p
(
y
∣
x
)
=
e
S
(
x
,
y
)
∑
y
′
∈
y
e
S
(
x
,
y
′
)
p(\mathbf{y} \mid \mathbf{x})=\frac{e^{S(\mathbf{x}, \mathbf{y})}}{\sum_{y^{\prime} \in \mathbf{y}} e^{S\left(\mathbf{x}, \mathbf{y}^{\prime}\right)}}
p(y∣x)=∑y′∈yeS(x,y′)eS(x,y)
其中
y
′
\mathbf{y}\prime
y′表示所有可能的预测序列。转化为损失函数便是最小化如下的损失函数:
L
=
−
ln
(
p
(
y
∣
x
)
)
=
ln
∑
y
′
∈
y
e
S
(
x
,
y
′
)
−
S
(
x
,
y
)
\mathcal{L}=-\ln\left(p(\mathbf{y} \mid \mathbf{x})\right) = \ln \sum_{y^{\prime} \in \mathbf{y}} e^{S\left(\mathbf{x}, \mathbf{y}^{\prime}\right)}-S(\mathbf{x}, \mathbf{y})
L=−ln(p(y∣x))=lny′∈y∑eS(x,y′)−S(x,y)
模型实现
根据上述介绍,利用Pytorch实现的BiLSTM-CRF模型代码如下所示:
import torch
import torch.nn as nn
import torch.nn.functional as F
from torchcrf import CRF
class BiLSTMCRF(nn.Module):
def __init__(self,
output_size,
embed_size,
num_layers,
hidden_size,
drop_prob,
vocab_size,
use_crf=False):
super(BiLSTMCRF, self).__init__()
self.output_size = output_size
self.use_crf = use_crf
self.vocab_size = vocab_size
self.word_embs = nn.Embedding(self.vocab_size, embed_size)
# 定义BiLSTM层
self.bilstm = nn.LSTM(bidirectional=True,
num_layers=num_layers,
input_size=embed_size,
hidden_size=hidden_size,
batch_first=True,
dropout=drop_prob)
# 定义全连接层
self.fc = nn.Linear(2 * hidden_size, output_size)
# 定义CRF层
if use_crf:
self.crf = CRF(self.output_size, batch_first=True)
def forward(self, x, y, mask=None):
x = self.word_embs(x)
lstmout, _ = self.bilstm(x)
emissions = self.fc(lstmout)
if self.use_crf:
loss = -self.crf(emissions=emissions, tags=y, mask=mask)
else:
loss = F.cross_entropy(emissions.reshape(-1, self.output_size), y.reshape(-1))
return loss
def predict(self, x, mask=None):
x = self.word_embs(x)
lstmout, _ = self.bilstm(x)
emissions = self.fc(lstmout)
if self.use_crf:
preds = self.crf.decode(emissions, mask)
else:
preds = torch.argmax(emissions, dim=-1).detach().cpu().numpy()
return preds
if __name__ == "__main__":
pass
实验
本文的实验的环境为:
操作系统: Win10
Python版本:
Pytorch版本: 1.8
主要依赖库: seqeval-1.2.2, pytorch-crf-0.7.2
实验参数设置为:
params = {
"lr": 0.001,
"batch_size": 128,
"epochs": 50,
"output_size": len(LABEL),
"embed_size": 256,
"hidden_size": 256,
"num_layers": 2,
"drop_prob": 0.5,
"use_crf": True # 是否添加CRF
}
限于时间原因,并没有进行细致调参,仅随便设置了一组参数,然后对使用和不使用CRF的模型进行对比。下图为对应的实验结果,从结果可以看出,不管是单个独立的类别还是整体,添加了CRF的效果基本上都要比不加CRF要好,由此验证了CRF设计的有效性。
结语
完整源代码:地址
参考资料:
以上便是本文的全部内容,要是觉得不错的话,可以点个赞或关注一下博主,你们的支持是博主进步的不竭动力,当然要是有问题的话也敬请批评指正!!!