当前位置:   article > 正文

用BERT做命名实体识别任务

bert实现命名实体识别

命名实体识别NER任务是NLP的一个常见任务,

它是Named Entity Recognization的简称。

简单地说,就是识别一个句子中的各种 名称实体。

诸如:人名,地名,机构 等。

例如对于下面这句话:

小明对小红说:"你听说过安利吗?"

它的NER抽取结果如下:

  1. [{'entity''person',
  2.   'word''小明',
  3.   'start'0,
  4.   'end'2},
  5.  {'entity''person',
  6.   'word''小红',
  7.   'start'3,
  8.   'end'5},
  9.  {'entity''organization',
  10.   'word''安利',
  11.   'start'12,
  12.   'end'14}]

本质上NER是一个token classification任务, 需要把文本中的每一个token做一个分类。

那些不是命名实体的token,一般用大'O'表示。

值得注意的是,由于有些命名实体是由连续的多个token构成的,为了避免有两个连续的相同的命名实体无法区分,需要对token是否处于命名实体的开头进行区分。

例如,对于下面这句话。

我爱北京天安门

如果我们不区分token是否为命名实体的开头的话,可能会得到这样的token分类结果。

我(O) 爱(O) 北(Loc) 京(Loc) 天(Loc) 安(Loc) 门(Loc)

然后我们做后处理的时候,把类别相同的token连起来,会得到一个location实体 '北京天安门'。

但是,’北京‘ 和 ’天安门‘ 是两个不同的location实体,把它们区分开来更加合理一些. 因此我们可以这样对token进行分类。

我(O) 爱(O) 北(B-Loc) 京(I-Loc) 天(B-Loc) 安(I-Loc) 门(I-Loc)

我们用 B-Loc表示这个token是一个location实体的开始token,用I-Loc表示这个token是一个location实体的内部(包括中间以及结尾)token.

这样,我们做后处理的时候,就可以把 B-loc以及它后面的 I-loc连成一个实体。这样就可以得到’北京‘ 和 ’天安门‘ 是两个不同的location的结果了。

区分token是否是entity开头的好处是我们可以把连续的同一类别的的命名实体进行区分,坏处是分类数量会几乎翻倍(n+1->2n+1)。

在许多情况下,出现这种连续的同命名实体并不常见,但为了稳妥起见,区分token是否是entity开头还是十分必要的。

一,准备数据

公众号算法美食屋后台回复关键词:torchkeras,获取本文notebook代码和车道线数据集下载链接。

  1. import numpy as np 
  2. import pandas as pd 
  3. from transformers import BertTokenizer
  4. from torch.utils.data import DataLoader,Dataset 
  5. from transformers import DataCollatorForTokenClassification
  6. import datasets

1,数据加载

  1. datadir = "./data/cluener_public/"
  2. train_path = datadir+"train.json"
  3. val_path = datadir+"dev.json"
  4. dftrain = pd.read_json(train_path,lines=True)
  5. dfval = pd.read_json(train_path,lines=True)
  6. entities = ['address','book','company','game','government','movie',
  7.               'name','organization','position','scene']
  8. label_names = ['O']+['B-'+x for x in entities]+['I-'+x for x in entities]
  9. id2label = {i: label for i, label in enumerate(label_names)}
  10. label2id = {v: k for k, v in id2label.items()}
  1. text = dftrain["text"][43]
  2. label = dftrain["label"][43]
  3. print(text)
  4. print(label)
  1. 世上或许有两个人并不那么喜欢LewisCarroll的原著小说《爱丽斯梦游奇境》(
  2. {'book': {'《爱丽斯梦游奇境》': [[31, 39]]},
  3. 'name': {'LewisCarroll': [[14, 25]]}}

2,文本分词

  1. from transformers import BertTokenizer
  2. model_name = 'bert-base-chinese'
  3. tokenizer = BertTokenizer.from_pretrained(model_name)
  1. tokenized_input = tokenizer(text)
  2. print(tokenized_input["input_ids"])
[101, 686, 677, 2772,..., 518, 113, 102]
  1. #可以从id还原每个token对应的字符组合
  2. tokens = tokenizer.convert_ids_to_tokens(tokenized_input["input_ids"])
  3. for t in tokens:
  4.     print(t)
  1. [CLS]
  2. [UNK]
  3. (
  4. [SEP]

3,标签对齐

可以看到,经过文本分词后的token长度与文本长度并不相同,

主要有以下一些原因导致:一是BERT分词后会增加一些特殊字符如 [CLS],[SEP]

二是,还会有一些英文单词的subword作为一个 token. (如这个例子中的 'charles'),

此外,还有一些未在词典中的元素被标记为[UNK]会造成影响。

因此需要给这些token赋予正确的label不是一个容易的事情。

我们分两步走,第一步,把原始的dict形式的label转换成字符级别的char_label

第二步,再将char_label对齐到token_label

  1. # 把 label格式转化成字符级别的char_label
  2. def get_char_label(text,label):
  3.     char_label = ['O' for x in text]
  4.     for tp,dic in label.items():
  5.         for word,idxs in dic.items():
  6.             idx_start = idxs[0][0]
  7.             idx_end = idxs[0][1]
  8.             char_label[idx_start] = 'B-'+tp
  9.             char_label[idx_start+1:idx_end+1] = ['I-'+tp for x in range(idx_start+1,idx_end+1)]
  10.     return char_label
  1. char_label = get_char_label(text,label)
  2. for char,char_tp in zip(text,char_label):
  3.     print(char+'\t'+char_tp)
  1. 世 O
  2. 上 O
  3. 或 O
  4. 许 O
  5. 有 O
  6. 两 O
  7. 个 O
  8. 人 O
  9. 并 O
  10. 不 O
  11. 那 O
  12. 么 O
  13. 喜 O
  14. 欢 O
  15. L B-name
  16. e I-name
  17. w I-name
  18. i I-name
  19. s I-name
  20. C I-name
  21. a I-name
  22. r I-name
  23. r I-name
  24. o I-name
  25. l I-name
  26. l I-name
  27. 的 O
  28. 原 O
  29. 著 O
  30. 小 O
  31. 说 O
  32. 《 B-book
  33. 爱 I-book
  34. 丽 I-book
  35. 斯 I-book
  36. 梦 I-book
  37. 游 I-book
  38. 奇 I-book
  39. 境 I-book
  40. 》 I-book
  41. ( O
  1. def get_token_label(text, char_label, tokenizer):
  2.     tokenized_input = tokenizer(text)
  3.     tokens = tokenizer.convert_ids_to_tokens(tokenized_input["input_ids"])
  4.     
  5.     iter_tokens = iter(tokens)
  6.     iter_char_label = iter(char_label)  
  7.     iter_text = iter(text.lower()) 
  8.     token_labels = []
  9.     t = next(iter_tokens)
  10.     char = next(iter_text)
  11.     char_tp = next(iter_char_label)
  12.     while True:
  13.         #单个字符token(如汉字)直接赋给对应字符token
  14.         if len(t)==1:
  15.             assert t==char
  16.             token_labels.append(char_tp)   
  17.             try:
  18.                 char = next(iter_text)
  19.                 char_tp = next(iter_char_label)
  20.             except StopIteration:
  21.                 pass  
  22.         #添加的特殊token如[CLS],[SEP],排除[UNK]
  23.         elif t in tokenizer.special_tokens_map.values() and t!='[UNK]':
  24.             token_labels.append('O')              
  25.         elif t=='[UNK]':
  26.             token_labels.append(char_tp) 
  27.             #重新对齐
  28.             try:
  29.                 t = next(iter_tokens)
  30.             except StopIteration:
  31.                 break 
  32.             if t not in tokenizer.special_tokens_map.values():
  33.                 while char!=t[0]:
  34.                     try:
  35.                         char = next(iter_text)
  36.                         char_tp = next(iter_char_label)
  37.                     except StopIteration:
  38.                         pass    
  39.             continue
  40.         #其它长度大于1的token,如英文token
  41.         else:
  42.             t_label = char_tp
  43.             t = t.replace('##','') #移除因为subword引入的'##'符号
  44.             for c in t:
  45.                 assert c==char or char not in tokenizer.vocab
  46.                 if t_label!='O':
  47.                     t_label=char_tp
  48.                 try:
  49.                     char = next(iter_text)
  50.                     char_tp = next(iter_char_label)
  51.                 except StopIteration:
  52.                     pass    
  53.             token_labels.append(t_label) 
  54.         try:
  55.             t = next(iter_tokens)
  56.         except StopIteration:
  57.             break  
  58.             
  59.     assert len(token_labels)==len(tokens)
  60.     return token_labels
token_labels = get_token_label(text,char_label,tokenizer)
  1. for t,t_label in zip(tokens,token_labels):
  2.     print(t,'\t',t_label)
  1. [CLS] O
  2. 世 O
  3. 上 O
  4. 或 O
  5. 许 O
  6. 有 O
  7. 两 O
  8. 个 O
  9. 人 O
  10. 并 O
  11. 不 O
  12. 那 O
  13. 么 O
  14. 喜 O
  15. 欢 O
  16. [UNK] B-name
  17. 的 O
  18. 原 O
  19. 著 O
  20. 小 O
  21. 说 O
  22. 《 B-book
  23. 爱 I-book
  24. 丽 I-book
  25. 斯 I-book
  26. 梦 I-book
  27. 游 I-book
  28. 奇 I-book
  29. 境 I-book
  30. 》 I-book
  31. ( O
  32. [SEP] O

4,构建管道

dftrain.head()

51f8d021b4b43b9f4cb3db995417b477.png

  1. def make_sample(text,label,tokenizer):
  2.     sample = tokenizer(text)
  3.     char_label = get_char_label(text,label)
  4.     token_label = get_token_label(text,char_label,tokenizer)
  5.     sample['labels'] = [label2id[x] for x in token_label]
  6.     return sample
  1. from tqdm import tqdm 
  2. train_samples = [make_sample(text,label,tokenizer) for text,label in 
  3.                  tqdm(list(zip(dftrain['text'],dftrain['label'])))]
  4. val_samples = [make_sample(text,label,tokenizer) for text,label in 
  5.                  tqdm(list(zip(dfval['text'],dfval['label'])))]
  1. 100%|██████████| 10748/10748 [00:06<00:00, 1717.47it/s]
  2. 100%|██████████| 10748/10748 [00:06<00:00, 1711.10it/s]
  1. ds_train = datasets.Dataset.from_list(train_samples)
  2. ds_val = datasets.Dataset.from_list(val_samples)
  1. data_collator = DataCollatorForTokenClassification(tokenizer=tokenizer)
  2. dl_train = DataLoader(ds_train,batch_size=8,collate_fn=data_collator)
  3. dl_val = DataLoader(ds_val,batch_size=8,collate_fn=data_collator)
  1. for batch in dl_train:
  2.     break

二,定义模型

  1. from transformers import BertForTokenClassification
  2. net = BertForTokenClassification.from_pretrained(
  3.     model_name,
  4.     id2label=id2label,
  5.     label2id=label2id,
  6. )
  7. #冻结bert基模型参数
  8. for para in net.bert.parameters():
  9.     para.requires_grad_(False)
  10. print(net.config.num_labels) 
  11. #模型试算
  12. out = net(**batch)
  13. print(out.loss) 
  14. print(out.logits.shape)

ad597c4424d147c8f854813634a263b7.png

三,训练模型

  1. import torch 
  2. from torchkeras import KerasModel 
  3. #我们需要修改StepRunner以适应transformers的数据集格式
  4. class StepRunner:
  5.     def __init__(self, net, loss_fn, accelerator, stage = "train", metrics_dict = None, 
  6.                  optimizer = None, lr_scheduler = None
  7.                  ):
  8.         self.net,self.loss_fn,self.metrics_dict,self.stage = net,loss_fn,metrics_dict,stage
  9.         self.optimizer,self.lr_scheduler = optimizer,lr_scheduler
  10.         self.accelerator = accelerator
  11.         if self.stage=='train':
  12.             self.net.train() 
  13.         else:
  14.             self.net.eval()
  15.     
  16.     def __call__(self, batch):
  17.         
  18.         out = self.net(**batch)
  19.         
  20.         #loss
  21.         loss= out.loss
  22.         
  23.         #preds
  24.         preds =(out.logits).argmax(axis=2
  25.     
  26.         #backward()
  27.         if self.optimizer is not None and self.stage=="train":
  28.             self.accelerator.backward(loss)
  29.             self.optimizer.step()
  30.             if self.lr_scheduler is not None:
  31.                 self.lr_scheduler.step()
  32.             self.optimizer.zero_grad()
  33.         
  34.         all_loss = self.accelerator.gather(loss).sum()
  35.         
  36.         labels = batch['labels']
  37.         
  38.         #precision & recall
  39.         
  40.         precision =  (((preds>0)&(preds==labels)).sum())/(
  41.             torch.maximum((preds>0).sum(),torch.tensor(1.0).to(preds.device)))
  42.         recall =  (((labels>0)&(preds==labels)).sum())/(
  43.             torch.maximum((labels>0).sum(),torch.tensor(1.0).to(labels.device)))
  44.     
  45.         
  46.         all_precision = self.accelerator.gather(precision).mean()
  47.         all_recall = self.accelerator.gather(recall).mean()
  48.         
  49.         f1 = 2*all_precision*all_recall/torch.maximum(
  50.             all_recall+all_precision,torch.tensor(1.0).to(labels.device))
  51.         
  52.         #losses
  53.         step_losses = {self.stage+"_loss":all_loss.item(), 
  54.                        self.stage+'_precision':all_precision.item(),
  55.                        self.stage+'_recall':all_recall.item(),
  56.                        self.stage+'_f1':f1.item()
  57.                       }
  58.         
  59.         #metrics
  60.         step_metrics = {}
  61.         
  62.         if self.stage=="train":
  63.             if self.optimizer is not None:
  64.                 step_metrics['lr'] = self.optimizer.state_dict()['param_groups'][0]['lr']
  65.             else:
  66.                 step_metrics['lr'] = 0.0
  67.         return step_losses,step_metrics
  68.     
  69. KerasModel.StepRunner = StepRunner
  1. optimizer = torch.optim.AdamW(net.parameters(), lr=3e-5)
  2. keras_model = KerasModel(net,
  3.                    loss_fn=None,
  4.                    optimizer = optimizer
  5.                    )
  1. keras_model.fit(
  2.     train_data = dl_train,
  3.     val_data= dl_val,
  4.     ckpt_path='bert_ner.pt',
  5.     epochs=50,
  6.     patience=5,
  7.     monitor="val_f1"
  8.     mode="max",
  9.     plot = True,
  10.     wandb = False,
  11.     quiet = True
  12. )

713beb26fe8fd7b653e5e0aacab9a0d5.png

四,评估模型

from torchmetrics import Accuracy
  1. acc = Accuracy(task='multiclass',num_classes=21)
  2. acc = keras_model.accelerator.prepare(acc)
  3. dl_test = keras_model.accelerator.prepare(dl_val)
  4. net = keras_model.accelerator.prepare(net)
  1. from tqdm import tqdm 
  2. for batch in tqdm(dl_test):
  3.     with torch.no_grad():
  4.         outputs = net(**batch)
  5.         
  6.     labels = batch['labels']
  7.     labels[labels<0]=0
  8.     #preds
  9.     preds =(outputs.logits).argmax(axis=2
  10.     acc.update(preds,labels)
acc.compute()  #这里的acc包括了 ’O‘的分类结果,存在高估。
tensor(0.9178, device='cuda:0')

五,使用模型

我们可以使用pipeline来串起整个预测流程.

注意我们这里使用内置的'simple'这个aggregation_strategy,

把应该归并的token自动归并成一个entity.

from transformers import pipeline
  1. recognizer = pipeline("token-classification"
  2.                       model=net, tokenizer=tokenizer, aggregation_strategy='simple')
net.to('cpu');
recognizer('小明对小红说,“你听说过安利吗?”')
  1. [{'entity_group': 'name',
  2. 'score': 0.6913842,
  3. 'word': '小 明',
  4. 'start': None,
  5. 'end': None},
  6. {'entity_group': 'name',
  7. 'score': 0.58951116,
  8. 'word': '小 红',
  9. 'start': None,
  10. 'end': None},
  11. {'entity_group': 'name',
  12. 'score': 0.74060774,
  13. 'word': '安 利',
  14. 'start': None,
  15. 'end': None}]

六,保存模型

保存model和tokenizer之后,我们可以用一个pipeline加载,并进行批量预测。

  1. net.save_pretrained("ner_bert")
  2. tokenizer.save_pretrained("ner_bert")
  1. ('ner_bert/tokenizer_config.json',
  2. 'ner_bert/special_tokens_map.json',
  3. 'ner_bert/vocab.txt',
  4. 'ner_bert/added_tokens.json')
  1. recognizer = pipeline("token-classification"
  2.                       model="ner_bert",
  3.                       aggregation_strategy='simple')
recognizer('小明对小红说,“你听说过安利吗?”')
  1. [{'entity_group': 'name',
  2. 'score': 0.6913842,
  3. 'word': '小 明',
  4. 'start': 0,
  5. 'end': 2},
  6. {'entity_group': 'name',
  7. 'score': 0.58951116,
  8. 'word': '小 红',
  9. 'start': 3,
  10. 'end': 5},
  11. {'entity_group': 'name',
  12. 'score': 0.74060774,
  13. 'word': '安 利',
  14. 'start': 12,
  15. 'end': 14}]

公众号后台回复关键词:torchkeras,获取本文notebook源码数据集以及更多有趣炼丹范例~

d3ebc76c457278f76c27ceba3990b48d.png

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/352710
推荐阅读
相关标签
  

闽ICP备14008679号