分享

保姆级教程,用PyTorch和BERT进行命名实体识别

 汉无为 2022-06-17 发布于湖北

图片
本文中,小猴子和大家一起学习如何预训练 BERT 模型来识别文本中每个单词的实体。

在处理 NLP 问题时,BERT 经常作为一种机器学习模型出现,我们可以依靠它的性能。事实上,它已经对超过 2,500M 的单词进行了预训练,并且其从单词序列中学习信息的双向特性使其成为一个强大的模型。

小猴子之前写过关于如何利用 BERT 进行文本分类的文章:保姆级教程,用PyTorch和BERT进行文本分类,在本文中,我们将更多地关注如何将 BERT 用于命名实体识别 (NER) 任务。

什么是NER?

NER 是 NLP 中的一项基础有很重要的任务,正所谓流水的NLP,铁打的NER。NER(实体识别任务)指的是识别出文本中的具有特定意义的短语或词。实体可以是单个词,甚至可以是指代同一类别的一组词,通常包括人名、地名、组织名、机构名和时间等。

例如,假设我们下面的句子,我们想从这个句子中提取有关地名的信息。

图片

NER 任务的第一步是检测实体。这可以是指代同一类别的一个词或一组词。举个例子:

  • '天安门' 由单个单词组成的实体
  • '北京天安门'由两个词组成的实体,但它们指的是同一类别。

为了确保 BERT 模型知道一个实体可以是单个词或一组词,那么我们需要通过所谓的Inside-Outside-Beginning (IOB)  标记在训练数据上提供有关实体开始和结束的信息。

识别到实体后,NER 任务的下一步是对实体进行分类。根据我们的用例,实体的类别可以是任何东西。以下是实体类别的示例:

  • 人物:云朵君,小明,小猴子,詹姆斯,吴恩达
  • 地点:北京,成都,上海,深圳,天府广场
  • 组织机构:北京大学,华为,腾讯,华西医院

命名实体识别的数据标注方式

NER是一种序列标注问题,因此他们的数据标注方式也遵照序列标注问题的方式,主要是BIO和BIOES两种。这里直接介绍BIOES:

  • B,即Begin,表示开始
  • I, 即Intermediate,表示中间
  • E,即End,表示结尾
  • S,即Single,表示单个字符
  • O,即Other,表示其他,用于标记无关字符

BERT 用于 NER

运用 BERT 解决与 NLP 相关的任务,是非常方便的。

图片

如果你还不熟悉 BERT,我建议你在阅读本文之前阅读我之前关于使用 BERT 进行文本分类的文章。在那里,详细介绍了有关 BERT 模型架构、模型期望的输入数据类型以及将从模型中获得的输出的信息。

BERT模型在文本分类和 NER 问题中的区别在于如何设置模型的输出。对于文本分类问题,仅使用特殊 [CLS] token 的 Embedding 向量输出。而 NER 任务中,需要使用所有 token 的 Embedding向量输出,希望模型预测每个 token 的实体,则通过使用所有token 的 Embedding向量输出。

关于数据集

在本文中使用的数据集是 CoNLL-2003 数据集,它是专门用于 NER 任务的数据集。你可以通过下面的链接下载 Kaggle 上的数据。

NER数据命名实体识别数据

import pandas as pd
df = pd.read_csv('ner.csv')
df.head()
图片

如图所示,有一个由文本和标签组成的数据框。标签对应于文本中每个单词的实体类别。

总共有9个实体类别,分别是:

  • geo ---> 地理实体
  • org ---> 组织实体
  • per ---> 个人实体
  • gpe ---> 地缘政治实体
  • tim ---> 时间指示器实体
  • art ---> 工件实体
  • eve ---> 事件实体
  • nat ---> 自然现象实体
  • O ---> 该单词不属于任何实体。

看一下数据集上可用的唯一标签:

# 根据空格拆分标签,并将它们转换为列表
labels = [i.split() for i in df['labels'].values.tolist()]
# 检查数据集中有多少标签
unique_labels = set()
for lb in labels:
  [unique_labels.add(i) for i in lb if i not in unique_labels]
print(unique_labels)
{'B-tim', 'B-art', 'I-art', 'O', 'I-gpe',
'I-per', 'I-nat', 'I-geo', 'B-eve',
'B-org', 'B-gpe', 'I-eve', 'B-per',
'I-tim', 'B-nat', 'B-geo', 'I-org'}

将每个标签映射到它的id表示,反之亦然:

labels_to_ids = {k: v for v, k in enumerate(sorted(unique_labels))}
ids_to_labels = {v: k for v, k in enumerate(sorted(unique_labels))}
print(labels_to_ids)
{'B-art': 0, 'B-eve': 1, 'B-geo': 2,
'B-gpe': 3, 'B-nat': 4, 'B-org': 5,
'B-per': 6, 'B-tim': 7, 'I-art': 8,
'I-eve': 9, 'I-geo': 10, 'I-gpe': 11,
'I-nat': 12, 'I-org': 13, 'I-per': 14,
'I-tim': 15, 'O': 16}

注意到,每个实体类别都以字母I或开头B。这对应于前面提到的 IOB 标记。I 表示 Intermediate 以及 B 表示 Beginning。看一下下面的句子进一步了解 IOB 标记的概念。

图片
  • 'Kevin'有B-pers标签,它是个人实体的开始
  • 'Durant'有I-pers标签,它是个人实体的延续
  • 'Brooklyn'有B-org标签,它是一个组织实体的开始
  • 'Nets' 有I-org标签,它是组织实体的延续
  • 其他词被分配O标签,它们不属于任何实体

数据预处理

在能够使用 BERT 模型对 token 级别的实体进行分类之前,需要先进行数据预处理,包括两部分:tokenization 和调整标签以匹配 tokenization。

Tokenization

使用 HuggingFace 的预训练 BERT 基础模型中的类BertTokenizerFast,可以轻松实现 tokenization。

为了给你一个例子,BERT 标记器是如何工作的,让我们看一下我们数据集中的一个文本:

text = df['text'].values.tolist()
example = text[36]
print(example)
'Prime Minister Geir Haarde has refused to
resign or call for early elections.'

对上面的文本进行标记BertTokenizerFast非常简单:

from transformers import BertTokenizerFast
tokenizer = BertTokenizerFast.from_pretrained('bert-base-cased')
text_tokenized = tokenizer(example, padding='max_length',
                           max_length=512, truncation=True,
                           return_tensors='pt')

从上面的BertTokenizerFast类调用tokenizer方法时,提供了几个参数:

  • padding: 用特殊的 [PAD] token将序列填充到指定的最大长度(BERT 模型的最大序列长度为 512)。
  • max_length: 序列的最大长度。
  • truncation: 这是一个布尔值。如果将该值设置为 True,则不会使用超过最大长度的token。
  • return_tensors:返回的张量类型,取决于我们使用的机器学习框架。由于我们使用的是 PyTorch,所以我们使用pt

以下是标记化过程的输出:

print(text_tokenized)

上下滑动查看更多

{'input_ids': tensor([[  101,  3460,  2110,   144,  6851,  1197, 11679,  2881,  1162,  1144,
  3347,  1106, 13133,  1137,  1840,  1111,  1346,  3212,   119,   102,
  0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
  0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
  0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
  0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
  .............................
  0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
  0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
  0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
  0,     0]]), 
  'token_type_ids': tensor([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  ..................................
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0]]), 
  'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  ..................................
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0]])}

从上面结果可见:从Tokenization输出是一个字典,其中包含三个变量:

  • input_ids:序列中标记的 id 表示。在 BERT 中,101 为特殊 [CLS] token 保留的id,id 102 为特殊**[SEP]** token保留的id,id 0 为**[PAD]** token保留的id。
  • token_type_ids:标识一个token所属的序列。由于每个文本只有一个序列,因此token_type_ids的所有值都将为 0。
  • attention_mask:标识一个token是真正的 token 还是 padding 得到的token。如果它是一个真正的token,则该值为 1,如果它是一个 [PAD] token,则该值为 0。

综上所述,可以使用'decode'方法,从上面的'input_ids'中将这些id解码回原始序列,如下所示:

print(tokenizer.decode(text_tokenized.input_ids[0]))
'[CLS] Prime Minister Geir Haarde has refused
to resign or call for early elections.
[SEP] [PAD] [PAD] [PAD] [PAD] ... [PAD]'

在实现decode方法后,我们得到了原始序列,并且是添加了来自 BERT 的特殊标记,例如序列开头的 [CLS] token,序列末尾的 [SEP] token,以及为了满足要求的最大长度 512 而设置的一堆 [PAD] token。

在 Tokenization 之后,需要进行调整每个 token 的标签。

Tokenization后调整标签

因为序列的长度不再匹配原始标签的长度,因此这是在Tokenization之后需要做的一个非常重要的步骤。

BERT 分词器在底层使用了所谓的 word-piece tokenizer,它是一个子词分词器。这意味着 BERT tokenizer 可能会将一个词拆分为一个或多个有意义的子词。

例如,还是使用上面的序列作为例子:

图片

上面的序列总共有 13 个标记,因此它也有 13 个标签。但是,在 BERT 标记化之后,我们得到以下结果:

print(tokenizer.convert_ids_to_tokens(text_tokenized['input_ids'][0]))
['[CLS]', 'Prime', 'Minister', 'G', '##ei',
'##r', 'Ha', '##ard', '##e', 'has', 'refused',
'to', 'resign', 'or', 'call', 'for', 'early',
'elections', '.', '[SEP]', '[PAD]', '[PAD]',
'[PAD]','[PAD]', ... , '[PAD]']

在Tokenization之后需要解决两个问题:

  • 添加来自 BERT 的特殊 token,例如 [CLS][SEP][PAD]
  • 一些 token 被子词分词器分成子词。

词片Tokenization将不常见的词拆分为它们的子词,例如上面示例中的' Geir '' Haarde '。这种词片Tokenization有助于 BERT 模型学习相关词的语义。

而这种词片Tokenization和 BERT 添加特殊token的结果是Tokenization后的序列长度不再匹配初始标签的长度。

从上面的例子来看,现在Tokenization后的序列中总共有 512 个token,而标签的长度仍然和以前一样。此外,序列中的第一个token不再是单词' Prime ',而是新添加的**[CLS]** token,因此我们也需要调整标签,以达到一一对应的结果。使其与标记化后的序列具有相同的长度。

如何实现标签调整呢?我们可以利用word_ids标记化结果中的方法如下:

word_ids = text_tokenized.word_ids()
print(tokenizer.convert_ids_to_tokens(text_tokenized['input_ids'][0]))
print(word_ids)
['[CLS]', 'Prime', 'Minister', 'G', '##ei',
'##r', 'Ha', '##ard', '##e', 'has', 'refused',
'to', 'resign', 'or', 'call', 'for', 'early',
'elections', '.', '[SEP]', '[PAD]', '[PAD]',
'[PAD]', '[PAD]', ..., '[PAD]']
[None, 0, 1, 2, 2, 2, 3, 3, 3, 4, 5, 6, 7, 8,
9, 10, 11, 12, 13, None, None, None, None,
..., None]

从上面可以看出,每个拆分的 token 共享相同的 word_ids,其中来自 BERT 的特殊 token,例如 [CLS][SEP][PAD] 都没有特定word_ids的,结果是None

通过这些 word_ids,并使用以下两种方法来调整标签的长度:

  1. 只为每个拆分token的第一个子词提供一个标签。子词的延续将简单地用'-100'作为标签。所有没有word_ids 的token也将标为 '-100'
  2. 在属于同一 token 的所有子词中提供相同的标签。所有没有word_ids的token都将标为 '-100'

下面的函数演示上面定义。

def align_label_example(tokenized_input, labels):
        word_ids = tokenized_input.word_ids()
        previous_word_idx = None
        label_ids = []   
        for word_idx in word_ids:
            if word_idx is None:
                label_ids.append(-100)                
            elif word_idx != previous_word_idx:
                try:
                  label_ids.append(labels_to_ids[labels[word_idx]])
                except:
                  label_ids.append(-100)        
            else:
                label_ids.append(labels_to_ids[labels[word_idx]] if label_all_tokens else -100)
            previous_word_idx = word_idx      
        return label_ids

如果要应用第一种方法,设置label_all_tokens为 False。如果要应用第二种方法,设置label_all_tokens为 True,如以下代码所示:

设置label_all_tokens=True

label = labels[36]
label_all_tokens = True

new_label = align_label_example(text_tokenized, label)
print(new_label)
print(tokenizer.convert_ids_to_tokens(text_tokenized['input_ids'][0]))
[-100, 16, 16, 6, 6, 6, 14, 14, 14, 16, 16,
16, 16, 16, 16, 16, 16, 16, 16, -100, -100,
-100, -100, ..., -100]
['[CLS]', 'Prime', 'Minister', 'G', '##ei',
'##r', 'Ha', '##ard', '##e', 'has', 'refused',
'to', 'resign', 'or', 'call', 'for', 'early',
'elections', '.', '[SEP]', '[PAD]', '[PAD]',
'[PAD]', '[PAD]', ..., '[PAD]']

设置label_all_tokens=False

label_all_tokens = False
new_label = align_label_example(text_tokenized, label)
print(new_label)
print(tokenizer.convert_ids_to_tokens(text_tokenized['input_ids'][0]))
[-100, 16, 16, 6, -100, -100, 14, -100, -100, 
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, -100,
-100, ..., -100]
['[CLS]', 'Prime', 'Minister', 'G', '##ei',
'##r', 'Ha', '##ard', '##e', 'has', 'refused',
'to', 'resign', 'or', 'call', 'for', 'early',
'elections', '.', '[SEP]', '[PAD]', '[PAD]',
'[PAD]', '[PAD]', ..., '[PAD]']

在本文的其余部分,我们将实现第一个方法,其中我们将只为每个token中的第一个子词提供一个标签并设置label_all_tokens=False

Dataset类

在为 NER 任务训练 BERT 模型之前,需要创建一个Dataset类来批量生成和获取数据。

上下滑动查看更多源码

import torch
def align_label(texts, labels):
    # 首先tokenizer输入文本
    tokenized_inputs = tokenizer(texts, padding='max_length', max_length=512, truncation=True)
  # 获取word_ids
    word_ids = tokenized_inputs.word_ids()

    previous_word_idx = None
    label_ids = []
    # 采用上述的第一中方法来调整标签,使得标签与输入数据对其。
    for word_idx in word_ids:
        # 如果token不在word_ids内,则用 “-100” 填充
        if word_idx is None:
            label_ids.append(-100)
        # 如果token在word_ids内,且word_idx不为None,则从labels_to_ids获取label id
        elif word_idx != previous_word_idx:
            try:
                label_ids.append(labels_to_ids[labels[word_idx]])
            except:
                label_ids.append(-100)
        # 如果token在word_ids内,且word_idx为None
        else:
            try:
                label_ids.append(labels_to_ids[labels[word_idx]] if label_all_tokens else -100)
            except:
                label_ids.append(-100)
        previous_word_idx = word_idx

    return label_ids
# 构建自己的数据集类
class DataSequence(torch.utils.data.Dataset):
    def __init__(self, df):
        # 根据空格拆分labels
        lb = [i.split() for i in df['labels'].values.tolist()]
        # tokenizer 向量化文本
        txt = df['text'].values.tolist()
        self.texts = [tokenizer(str(i),
                               padding='max_length', max_length = 512
                                truncation=True, return_tensors='pt'for i in txt]
        # 对齐标签
        self.labels = [align_label(i,j) for i,j in zip(txt, lb)]

    def __len__(self):
        return len(self.labels)

    def get_batch_data(self, idx):
        return self.texts[idx]

    def get_batch_labels(self, idx):
        return torch.LongTensor(self.labels[idx])

    def __getitem__(self, idx):
        batch_data = self.get_batch_data(idx)
        batch_labels = self.get_batch_labels(idx)
        return batch_data, batch_labels

在上面的代码中,在函数__init__中调用带有tokenizer变量的BertTokenizerFast类来标记输入文本,并align_label在Tokenization之后调整标签。

接下来,我们将数据随机拆分为训练集、验证集和测试集。由于数据总数为 47959,出于演示目的和加快训练过程,这里将只选取其中的 1000 个。当然,你也可以将所有数据用于模型训练。

import numpy as np
df = df[0:1000]
df_train, df_val, df_test = np.split(df.sample(frac=1, random_state=42),
                            [int(.8 * len(df)), int(.9 * len(df))])

构建模型

在本文中,使用来自 HuggingFace 的预训练 BERT 基础模型。既然我们要在token级别对文本进行分类,那么需要使用 BertForTokenClassification 类。

BertForTokenClassification 类是一个包装 BERT 模型并在 BERT 模型之上添加线性层的模型,将充当token级分类器。

from transformers import BertForTokenClassification
class BertModel(torch.nn.Module):
    def __init__(self):
        super(BertModel, self).__init__()
        self.bert = BertForTokenClassification.from_pretrained(
                       'bert-base-cased'
                                     num_labels=len(unique_labels))

    def forward(self, input_id, mask, label):
        output = self.bert(input_ids=input_id, attention_mask=mask,
                           labels=label, return_dict=False)
        return output

在上面的代码中,首先实例化模型并将每个token分类器的输出设置为等于我们数据集上唯一实体的数量(在我们的例子是 17)。

训练模型

这里使用标准的 PyTorch 训练循环训练 BERT 模型,如下所示:

上下滑动查看更多源码

def train_loop(model, df_train, df_val):
    # 定义训练和验证集数据
    train_dataset = DataSequence(df_train)
    val_dataset = DataSequence(df_val)
    # 批量获取训练和验证集数据
    train_dataloader = DataLoader(train_dataset, num_workers=4, batch_size=1, shuffle=True)
    val_dataloader = DataLoader(val_dataset, num_workers=4, batch_size=1)
    # 判断是否使用GPU,如果有,尽量使用,可以加快训练速度
    use_cuda = torch.cuda.is_available()
    device = torch.device('cuda' if use_cuda else 'cpu')
    # 定义优化器
    optimizer = SGD(model.parameters(), lr=LEARNING_RATE)

    if use_cuda:
        model = model.cuda()
    # 开始训练循环
    best_acc = 0
    best_loss = 1000
    for epoch_num in range(EPOCHS):

        total_acc_train = 0
        total_loss_train = 0
        # 训练模型
        model.train()
        # 按批量循环训练模型
        for train_data, train_label in tqdm(train_dataloader):
      # 从train_data中获取mask和input_id
            train_label = train_label[0].to(device)
            mask = train_data['attention_mask'][0].to(device)
            input_id = train_data['input_ids'][0].to(device)
            # 梯度清零!!
            optimizer.zero_grad()
            # 输入模型训练结果:损失及分类概率
            loss, logits = model(input_id, mask, train_label)
            # 过滤掉特殊token及padding的token
            logits_clean = logits[0][train_label != -100]
            label_clean = train_label[train_label != -100]
            # 获取最大概率值
            predictions = logits_clean.argmax(dim=1)
      # 计算准确率
            acc = (predictions == label_clean).float().mean()
            total_acc_train += acc
            total_loss_train += loss.item()
      # 反向传递
            loss.backward()
            # 参数更新
            optimizer.step()
        # 模型评估
        model.eval()

        total_acc_val = 0
        total_loss_val = 0
        for val_data, val_label in val_dataloader:
      # 批量获取验证数据
            val_label = val_label[0].to(device)
            mask = val_data['attention_mask'][0].to(device)
            input_id = val_data['input_ids'][0].to(device)
      # 输出模型预测结果
            loss, logits = model(input_id, mask, val_label)
      # 清楚无效token对应的结果
            logits_clean = logits[0][val_label != -100]
            label_clean = val_label[val_label != -100]
            # 获取概率值最大的预测
            predictions = logits_clean.argmax(dim=1)          
            # 计算精度
            acc = (predictions == label_clean).float().mean()
            total_acc_val += acc
            total_loss_val += loss.item()

        val_accuracy = total_acc_val / len(df_val)
        val_loss = total_loss_val / len(df_val)

        print(
            f'''Epochs: {epoch_num + 1} | 
                Loss: {total_loss_train / len(df_train): .3f} | 
                Accuracy: {total_acc_train / len(df_train): .3f} |
                Val_Loss: {total_loss_val / len(df_val): .3f} | 
                Accuracy: {total_acc_val / len(df_val): .3f}'''
)

LEARNING_RATE = 1e-2
EPOCHS = 5
model = BertModel()
train_loop(model, df_train, df_val)

在上面的训练循环中,只训练了 5 个 epoch 的模型,然后使用 SGD 作为优化器。使用BertForTokenClassification 类计算每个批次的损失。

注意,有一个重要的步骤!在训练循环的每个 epoch 中,在模型预测之后,需要忽略所有以 '-100' 作为标签的token。

下面是我们训练 BERT 模型 5 个 epoch 后的训练输出示例:

图片

当你训练自己的 BERT 模型时,你将看到的输出可能会有所不同,因为训练过程中存在随机性。

大家想想,如何提高我们模型的性能。例如在我们有一个数据不平衡问题,因为有很多带有'O'标签的token。可以通过在训练过程中添加不同类的权重来改进我们的模型。

此外还可以尝试不同的优化器,例如具有权重衰减正则化的 Adam 优化器。

评估模型

现在已经训练了的模型,接下来可以使用测试数据集来测试模型的性能。评估代码与验证代码类似,这里不做详细注释。

上下滑动查看更多源码

def evaluate(model, df_test):
    # 定义测试数据
    test_dataset = DataSequence(df_test)
    # 批量获取测试数据
    test_dataloader = DataLoader(test_dataset, num_workers=4, batch_size=1)
   # 使用GPU
    use_cuda = torch.cuda.is_available()
    device = torch.device('cuda' if use_cuda else 'cpu')
    if use_cuda:
        model = model.cuda()
    total_acc_test = 0.0
    for test_data, test_label in test_dataloader:
        test_label = test_label[0].to(device)
        mask = test_data['attention_mask'][0].to(device)
        input_id = test_data['input_ids'][0].to(device)
          
        loss, logits = model(input_id, mask, test_label.long())
        logits_clean = logits[0][test_label != -100]
        label_clean = test_label[test_label != -100]
        predictions = logits_clean.argmax(dim=1)             
        acc = (predictions == label_clean).float().mean()
        total_acc_test += acc
    val_accuracy = total_acc_test / len(df_test)
    print(f'Test Accuracy: {total_acc_test / len(df_test): .3f}')

evaluate(model, df_test)

就本案例而言,经过训练的模型在测试集上平均达到了 92.22% 的准确率。根据不同的任务或评价标准,可以选用 F1 分数、精度或召回率。

或者可以使用经过训练的模型来预测文本或句子中每个单词的实体,代码如下:

上下滑动查看更多源码

def align_word_ids(texts): 
    tokenized_inputs = tokenizer(texts, padding='max_length', max_length=512, truncation=True)
    word_ids = tokenized_inputs.word_ids()
    previous_word_idx = None
    label_ids = []
    for word_idx in word_ids:
        if word_idx is None:
            label_ids.append(-100)

        elif word_idx != previous_word_idx:
            try:
                label_ids.append(1)
            except:
                label_ids.append(-100)
        else:
            try:
                label_ids.append(1 if label_all_tokens else -100)
            except:
                label_ids.append(-100)
        previous_word_idx = word_idx
    return label_ids

def evaluate_one_text(model, sentence):

    use_cuda = torch.cuda.is_available()
    device = torch.device('cuda' if use_cuda else 'cpu')

    if use_cuda:
        model = model.cuda()

    text = tokenizer(sentence, padding='max_length', max_length = 512, truncation=True, return_tensors='pt')
    mask = text['attention_mask'][0].unsqueeze(0).to(device)
    input_id = text['input_ids'][0].unsqueeze(0).to(device)
    label_ids = torch.Tensor(align_word_ids(sentence)).unsqueeze(0).to(device)
    
    logits = model(input_id, mask, None)
    logits_clean = logits[0][label_ids != -100]
    
    predictions = logits_clean.argmax(dim=1).tolist()
    prediction_label = [ids_to_labels[i] for i in predictions]
    print(sentence)
    print(prediction_label)
            
evaluate_one_text(model, 'Bill Gates is the founder of Microsoft')

从结果看,我们的模型将能够很好地预测陌生句子中每个单词的实体。

结论

在本文中,我们为命名实体识别 (NER) 任务构建了 BERT 模型。并训练了 BERT 模型来预测token级别的自定义文本或自定义句子的 IOB token。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约