当前位置:   article > 正文

pytorch BiLSTM+CRF模型实现NER任务_torch lstm crf ner

torch lstm crf ner

本次实现BiLSTM+CRF模型的数据来源于DataFountain平台上的“产品评论观点提取”竞赛,数据仅用来做模型练习使用,并未参与实际竞赛评分。

 

竞赛地址:产品评论观点提取

1. 数据分析

数据分为测试集数据7528条,测试集数据(未统计)。

测试集数据共有四个属性,分别是:ID号,文本内容,BIO实体标签,class分类

本次比赛的任务一共分为两部分,第一部分是NER部分,采用BIO实体标签作为训练参考,另一部分为文本分类,目前只做了NER部分,因此暂时只针对NER部分讲解。

测试集数据具体如下:

 首先分析一下我们的text长度,对训练集中7525个样本的text长度进行统计,可以得到一下直方图:

从图中可以看出,文本长度为20左右的数据量最大,总体来说文本长度都在100个字符以内,所以我们可以把要放入模型训练的固定文本长度MAX_LEN设置为100。

要做NER任务,就需要把每个字符预测为某一类标签,加上O标签,我们的标签一共有九类,可以查看一下训练集中除了O以外的八类标签的频次情况,就可以知道是否有某一类标签的数据量过少,过少的话就有可能带来此类标签预测效果不好的结果。

从上图的频次直方图可以看到,出现频次最高的是'I-PRODUCT',共有6000多次,频次最低的是'B-BANK',共有不到2000次 

不过总的来说差距不算太悬殊,tag的分布可以说是较为均匀的,问题不大。

2. BiLSTM+CRF模型

 对于NER任务,较为常用的模型有HMM、CRF等机器学习方法,(Bi)LSTM+CRF,CNN+CRF,BERT+CRF,后续我会记录一下BERT+CRF等等模型的实现,首先从BiLSTM+CRF开始。

 (1)BiLSTM+CRF模型示意图:

 模型的输入为固定长度的文本,文本中的每个词向量为Wi。经过BiLSTM层训练后进入全连接层,就可以得到每个词在每个tag位置的概率了,因为我们总共有九个tag,所以全连接层的输出的最低维度就是长度为9的向量。最后经过CRF层训练后,就可以输出loss值;如果是预测的话,使用CRF层的decode方法,就可以得到每个词具体预测的tag了。

(2)模型各层数据结构的变化示意图:

之所以画有这个数据结构变化流程,是因为我个人非常纠结在模型变化中的数据结构变化(可能是我菜..),但是在参考网上别的资料的时候,基本上没有见过有提供这类总结的,所以我就画个图,万一有人和我一样需要呢[doge]

上图中,输入的结构是[batch_size, seq_len],batch_size就是数据集每个batch的大小了这个很简单,seq_len是文本长度,一般文本长度都是固定的(短于固定长度的话就需要padding)。输入数据实际上就是经过词转index,再经过padding过后的训练集/验证集数据了。

这里有一点需要注意:输入数据的结构中,我们一般第一个维度都是batch_size,但是实际上pytorch的各层模型中,它们默认的数据输入参数都是batch_first=False,因此后面就需要将数据转换成[seq_len, batch_size]的结构。

另外非常重要的是,由[batch_size,seq_len]转[seq_len, batch_size],不要用tensor类的view方法,也不要用numpy中的reshape方法,这样转换的维度是不正确的(你可以试试),应该要用tensor类的permute方法来转换维度。

图中中间部分的转换流程就不讲了,没什么太多问题,最后经过CRF层的时候,如果是进行训练,那么需要参数emissions和tags,输出结果就是loss值,不太一样的是这里的loss值应该是进行了一个-log的操作,因此直接输出的loss值就会变成复数,为了能够用常用的优化器进行参数优化,这里的loss值需要乘以一个-1;如果是进行预测,就需要调用decode方法。

3. 具体实现

模型使用pytorch实现,jupyter notebook版本的完整代码在github上:NLP-NER-models

整体实现和核心代码如下:

(1)词转index&填充长度不足/截取过长的文本

  1. MAX_LEN = 100 #句子的标准长度
  2. BATCH_SIZE = 8 #minibatch的大小
  3. EMBEDDING_DIM = 120
  4. HIDDEN_DIM = 12
  5. # 获取 tag to index 词典
  6. def get_tag2index():
  7. return {"O": 0,
  8. "B-BANK":1,"I-BANK":2, #银行实体
  9. "B-PRODUCT":3,"I-PRODUCT":4, #产品实体
  10. "B-COMMENTS_N":5,"I-COMMENTS_N":6, #用户评论,名词
  11. "B-COMMENTS_ADJ":7,"I-COMMENTS_ADJ":8 #用户评论,形容词
  12. }
  13. # 获取 word to index 词典
  14. def get_w2i(vocab_path = dicPath):
  15. w2i = {}
  16. with open(vocab_path, encoding = 'utf-8') as f:
  17. while True:
  18. text = f.readline()
  19. if not text:
  20. break
  21. text = text.strip()
  22. if text and len(text) > 0:
  23. w2i[text] = len(w2i) + 1
  24. return w2i
  25. def pad2mask(t):
  26. if t==pad_index: #转换成mask所用的0
  27. return 0
  28. else:
  29. return 1
  30. def text_tag_to_index(dataset):
  31. texts = []
  32. labels = []
  33. masks = []
  34. for row in range(len(dataset)):
  35. text = dataset.iloc[row]['text']
  36. tag = dataset.iloc[row]['BIO_anno']
  37. #text
  38. #tag
  39. if len(text)!=len(tag): #如果从数据集获得的text和label长度不一致
  40. next
  41. #1. word转index
  42. #1.1 text词汇
  43. text_index = []
  44. text_index.append(start_index) #先加入开头index
  45. for word in text:
  46. text_index.append(w2i.get(word, unk_index)) #将当前词转成词典对应index,或不认识标注UNK的index
  47. text_index.append(end_index) #最后加个结尾index
  48. #index
  49. #1.2 tag标签
  50. tag = tag.split()
  51. tag_index = [tag2i.get(t,0) for t in tag]
  52. tag_index = [0] + tag_index + [0]
  53. #2. 填充或截至句子至标准长度
  54. #2.1 text词汇&tag标签
  55. if len(text_index)<MAX_LEN: #句子短,补充pad_index到满够MAX_LEN
  56. pad_len = MAX_LEN-len(text_index)
  57. text_index = text_index + [pad_index]*pad_len
  58. tag_index = tag_index + [0]*pad_len
  59. elif len(text_index)>MAX_LEN: #句子过长,截断
  60. text_index = text_index[:MAX_LEN-1]
  61. text_index.append(end_index)
  62. tag_index = tag_index[:MAX_LEN-1]
  63. tag_index.append(0)
  64. masks.append([pad2mask(t) for t in text_index])
  65. texts.append(text_index)
  66. labels.append(tag_index)
  67. #把list类型的转成tensor类型,方便后期进行训练
  68. texts = torch.LongTensor(texts)
  69. labels = torch.LongTensor(labels)
  70. masks = torch.tensor(masks, dtype=torch.uint8)
  71. return texts,labels,masks
  72. #unk:未知词 pad:填充 start:文本开头 end:文本结束
  73. unk_flag = '[UNK]'
  74. pad_flag = '[PAD]'
  75. start_flag = '[STA]'
  76. end_flag = '[END]'
  77. w2i = get_w2i() #获得word_to_index词典
  78. tag2i = get_tag2index() #获得tag_to_index词典
  79. #获得各flag的index值
  80. unk_index = w2i.get(unk_flag, 101)
  81. pad_index = w2i.get(pad_flag, 1)
  82. start_index = w2i.get(start_flag, 102) #开始
  83. end_index = w2i.get(end_flag, 103) #中间截至(主要用在有上下句的情况下)
  84. #将训练集的字符全部转成index,并改成MAX_LEN长度
  85. texts,labels,masks = text_tag_to_index(train_dataset)

(2)pytorch BiLSTM+CRF模型设置

  1. class BiLSTM_CRF(nn.Module):
  2. def __init__(self, vocab_size, tag_to_ix, embedding_dim, hidden_dim, pad_index,batch_size):
  3. super(BiLSTM_CRF, self).__init__()
  4. self.embedding_dim = embedding_dim
  5. self.hidden_dim = hidden_dim
  6. self.vocab_size = vocab_size
  7. self.tag_to_ix = tag_to_ix
  8. self.tagset_size = len(tag_to_ix)
  9. self.pad_idx = pad_index
  10. self.batch_size = batch_size
  11. #####中间层设置
  12. #embedding层
  13. self.word_embeds = nn.Embedding(vocab_size,embedding_dim,padding_idx=self.pad_idx) #转词向量
  14. #lstm层
  15. self.lstm = nn.LSTM(embedding_dim, hidden_dim//2, num_layers = 1, bidirectional = True)
  16. #LSTM的输出对应tag空间(tag space)
  17. self.hidden2tag = nn.Linear(hidden_dim, self.tagset_size) #输入是[batch_size, size]中的size,输出是[batch_size,output_size]的output_size
  18. #CRF层
  19. self.crf = CRF(self.tagset_size) #默认batch_first=False
  20. def forward(self, sentence, tags=None, mask=None): #sentence=(batch,seq_len) tags=(batch,seq_len)
  21. self.batch_size = sentence.shape[0] #防止最后一batch中的数据量不够原本BATCH_SIZE
  22. #1. 从sentence到Embedding层
  23. embeds = self.word_embeds(sentence).permute(1,0,2)#.view(MAX_LEN,len(sentence),-1) #output=[seq_len, batch_size, embedding_size]
  24. #2. 从Embedding层到BiLSTM层
  25. self.hidden = (torch.randn(2,self.batch_size,self.hidden_dim//2),torch.randn(2,self.batch_size,self.hidden_dim//2)) #修改进来 shape=((2,1,2),(2,1,2))
  26. lstm_out, self.hidden = self.lstm(embeds, self.hidden)
  27. #3. 从BiLSTM层到全连接层
  28. #从lstm的输出转为tagset_size长度的向量组(即输出了每个tag的可能性)
  29. lstm_feats = self.hidden2tag(lstm_out)
  30. #4. 全连接层到CRF层
  31. if tags is not None: #训练用 #mask=attention_masks.byte()
  32. if mask is not None:
  33. loss = -1.*self.crf(emissions=lstm_feats,tags=tags.permute(1,0),mask=mask.permute(1,0),reduction='mean') #outputs=(batch_size,) 输出log形式的likelihood
  34. else:
  35. loss = -1.*self.crf(emissions=lstm_feats,tags=tags.permute(1,0),reduction='mean')
  36. return loss
  37. else: #测试用
  38. if mask is not None:
  39. prediction = self.crf.decode(emissions=lstm_feats,mask=mask.permute(1,0)) #mask=attention_masks.byte()
  40. else:
  41. prediction = self.crf.decode(emissions=lstm_feats)
  42. return prediction
  43. #创建模型和优化器
  44. model = BiLSTM_CRF(len(w2i), tag2i, EMBEDDING_DIM, HIDDEN_DIM,pad_index,BATCH_SIZE)
  45. optimizer = optim.SGD(model.parameters(), lr=0.01, weight_decay=1e-4)
  46. #显示模型基本参数
  47. model

Embedding层将词indx转词向量,torch.nn.Embedding方法应该是采用随机变量确定embedding向量的;

LSTM层,隐藏层节点数6,因是BiLSTM所以需要乘以2,即共12个hidden units;

全连接层,由BiLSTM输出的向量转入全连接层,输出维度为tag个数;

CRF层。

采用SGD梯度优化方法进行参数优化,learning rate设为0.01(没经过特别研究),weight_decay设为1e-4。

(3)训练

  1. samples_cnt = texts.shape[0]
  2. batch_cnt = math.ceil(samples_cnt/BATCH_SIZE) #整除 向上取整
  3. loss_list = []
  4. for epoch in range(10):
  5. for step, batch_data in enumerate(train_loader):
  6. # 1. 清空梯度
  7. model.zero_grad()
  8. # 2. 运行模型
  9. loss = model(batch_data['texts'], batch_data['labels'],batch_data['masks'])
  10. if step%100 ==0:
  11. logger.info('Epoch=%d step=%d/%d loss=%.5f' % (epoch,step,batch_cnt,loss))
  12. # 3. 计算loss值,梯度并更新权重参数
  13. loss.backward() #retain_graph=True) #反向传播,计算当前梯度
  14. optimizer.step() #根据梯度更新网络参数
  15. loss_list.append(loss)

(4)验证集进行验证

因为这次使用的数据集没有验证集,所以在开始时把训练集按7:3分为训练集和验证集,把分离开的验证集进行测试,看最后的F1-Score值评分情况。

  1. #batch_masks:tensor数据,结构为(batch_size,MAX_LEN)
  2. #batch_labels: tensor数据,结构为(batch_size,MAX_LEN)
  3. #batch_prediction:list数据,结构为(batch_size,) #每个数据长度不一(在model参数mask存在的情况下)
  4. def f1_score_evaluation(batch_masks,batch_labels,batch_prediction):
  5. all_prediction = []
  6. all_labels = []
  7. batch_size = batch_masks.shape[0] #防止最后一batch的数据不够batch_size
  8. for index in range(batch_size):
  9. #把没有mask掉的原始tag都集合到一起
  10. length = sum(batch_masks[index].numpy()==1)
  11. _label = batch_labels[index].numpy().tolist()[:length]
  12. all_labels = all_labels+_label
  13. #把没有mask掉的预测tag都集合到一起
  14. #_predict = y_pred[index][:length]
  15. all_prediction = all_prediction+y_pred[index]
  16. assert len(_label)==len(y_pred[index])
  17. assert len(all_prediction) == len(all_labels)
  18. score = f1_score(all_prediction,all_labels,average='weighted')
  19. return score
  20. #把每个batch的数据都验证一遍,取均值
  21. model.eval() #不启用 BatchNormalization 和 Dropout,保证BN和dropout不发生变化
  22. score_list = []
  23. for step, batch_data in enumerate(test_loader):
  24. with torch.no_grad(): #这部分的代码不用跟踪反向梯度更新
  25. y_pred = model(sentence=batch_data['texts'],mask=batch_data['masks'])
  26. score = f1_score_evaluation(batch_masks=batch_data['masks'],
  27. batch_labels=batch_data['labels'],
  28. batch_prediction=y_pred)
  29. score_list.append(score)
  30. #score_list
  31. logger.info("average-f1-score:"+str(np.mean(score_list)))

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

闽ICP备14008679号