pytorch BiLSTM+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&填充長度不足/截取過長的文本
MAX_LEN = 100 #句子的標準長度 BATCH_SIZE = 8 #minibatch的大小 EMBEDDING_DIM = 120 HIDDEN_DIM = 12# 獲取 tag to index 詞典 def get_tag2index():return {"O": 0,"B-BANK":1,"I-BANK":2, #銀行實體"B-PRODUCT":3,"I-PRODUCT":4, #產品實體"B-COMMENTS_N":5,"I-COMMENTS_N":6, #用戶評論,名詞"B-COMMENTS_ADJ":7,"I-COMMENTS_ADJ":8 #用戶評論,形容詞} # 獲取 word to index 詞典 def get_w2i(vocab_path = dicPath):w2i = {}with open(vocab_path, encoding = 'utf-8') as f:while True:text = f.readline()if not text:breaktext = text.strip()if text and len(text) > 0:w2i[text] = len(w2i) + 1return w2idef pad2mask(t):if t==pad_index: #轉換成mask所用的0return 0else:return 1def text_tag_to_index(dataset):texts = []labels = []masks = []for row in range(len(dataset)):text = dataset.iloc[row]['text']tag = dataset.iloc[row]['BIO_anno']#text#tagif len(text)!=len(tag): #如果從數據集獲得的text和label長度不一致next#1. word轉index#1.1 text詞匯text_index = []text_index.append(start_index) #先加入開頭indexfor word in text:text_index.append(w2i.get(word, unk_index)) #將當前詞轉成詞典對應index,或不認識標注UNK的indextext_index.append(end_index) #最后加個結尾index#index#1.2 tag標簽tag = tag.split()tag_index = [tag2i.get(t,0) for t in tag]tag_index = [0] + tag_index + [0]#2. 填充或截至句子至標準長度#2.1 text詞匯&tag標簽if len(text_index)<MAX_LEN: #句子短,補充pad_index到滿夠MAX_LENpad_len = MAX_LEN-len(text_index)text_index = text_index + [pad_index]*pad_lentag_index = tag_index + [0]*pad_lenelif len(text_index)>MAX_LEN: #句子過長,截斷text_index = text_index[:MAX_LEN-1]text_index.append(end_index)tag_index = tag_index[:MAX_LEN-1]tag_index.append(0)masks.append([pad2mask(t) for t in text_index])texts.append(text_index)labels.append(tag_index)#把list類型的轉成tensor類型,方便后期進行訓練texts = torch.LongTensor(texts)labels = torch.LongTensor(labels)masks = torch.tensor(masks, dtype=torch.uint8)return texts,labels,masks#unk:未知詞 pad:填充 start:文本開頭 end:文本結束 unk_flag = '[UNK]' pad_flag = '[PAD]' start_flag = '[STA]' end_flag = '[END]' w2i = get_w2i() #獲得word_to_index詞典 tag2i = get_tag2index() #獲得tag_to_index詞典#獲得各flag的index值 unk_index = w2i.get(unk_flag, 101) pad_index = w2i.get(pad_flag, 1) start_index = w2i.get(start_flag, 102) #開始 end_index = w2i.get(end_flag, 103) #中間截至(主要用在有上下句的情況下)#將訓練集的字符全部轉成index,并改成MAX_LEN長度 texts,labels,masks = text_tag_to_index(train_dataset)(2)pytorch BiLSTM+CRF模型設置
class BiLSTM_CRF(nn.Module):def __init__(self, vocab_size, tag_to_ix, embedding_dim, hidden_dim, pad_index,batch_size):super(BiLSTM_CRF, self).__init__()self.embedding_dim = embedding_dimself.hidden_dim = hidden_dimself.vocab_size = vocab_sizeself.tag_to_ix = tag_to_ixself.tagset_size = len(tag_to_ix)self.pad_idx = pad_indexself.batch_size = batch_size#####中間層設置#embedding層self.word_embeds = nn.Embedding(vocab_size,embedding_dim,padding_idx=self.pad_idx) #轉詞向量#lstm層self.lstm = nn.LSTM(embedding_dim, hidden_dim//2, num_layers = 1, bidirectional = True)#LSTM的輸出對應tag空間(tag space)self.hidden2tag = nn.Linear(hidden_dim, self.tagset_size) #輸入是[batch_size, size]中的size,輸出是[batch_size,output_size]的output_size#CRF層self.crf = CRF(self.tagset_size) #默認batch_first=Falsedef forward(self, sentence, tags=None, mask=None): #sentence=(batch,seq_len) tags=(batch,seq_len)self.batch_size = sentence.shape[0] #防止最后一batch中的數據量不夠原本BATCH_SIZE#1. 從sentence到Embedding層embeds = self.word_embeds(sentence).permute(1,0,2)#.view(MAX_LEN,len(sentence),-1) #output=[seq_len, batch_size, embedding_size]#2. 從Embedding層到BiLSTM層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)) lstm_out, self.hidden = self.lstm(embeds, self.hidden) #3. 從BiLSTM層到全連接層#從lstm的輸出轉為tagset_size長度的向量組(即輸出了每個tag的可能性)lstm_feats = self.hidden2tag(lstm_out) #4. 全連接層到CRF層if tags is not None: #訓練用 #mask=attention_masks.byte()if mask is not None:loss = -1.*self.crf(emissions=lstm_feats,tags=tags.permute(1,0),mask=mask.permute(1,0),reduction='mean') #outputs=(batch_size,) 輸出log形式的likelihoodelse:loss = -1.*self.crf(emissions=lstm_feats,tags=tags.permute(1,0),reduction='mean')return losselse: #測試用if mask is not None:prediction = self.crf.decode(emissions=lstm_feats,mask=mask.permute(1,0)) #mask=attention_masks.byte()else:prediction = self.crf.decode(emissions=lstm_feats)return prediction#創建模型和優化器 model = BiLSTM_CRF(len(w2i), tag2i, EMBEDDING_DIM, HIDDEN_DIM,pad_index,BATCH_SIZE) optimizer = optim.SGD(model.parameters(), lr=0.01, weight_decay=1e-4) #顯示模型基本參數 modelEmbedding層將詞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)訓練
samples_cnt = texts.shape[0] batch_cnt = math.ceil(samples_cnt/BATCH_SIZE) #整除 向上取整 loss_list = [] for epoch in range(10):for step, batch_data in enumerate(train_loader):# 1. 清空梯度model.zero_grad()# 2. 運行模型loss = model(batch_data['texts'], batch_data['labels'],batch_data['masks']) if step%100 ==0:logger.info('Epoch=%d step=%d/%d loss=%.5f' % (epoch,step,batch_cnt,loss))# 3. 計算loss值,梯度并更新權重參數 loss.backward() #retain_graph=True) #反向傳播,計算當前梯度optimizer.step() #根據梯度更新網絡參數loss_list.append(loss)(4)驗證集進行驗證
因為這次使用的數據集沒有驗證集,所以在開始時把訓練集按7:3分為訓練集和驗證集,把分離開的驗證集進行測試,看最后的F1-Score值評分情況。
#batch_masks:tensor數據,結構為(batch_size,MAX_LEN) #batch_labels: tensor數據,結構為(batch_size,MAX_LEN) #batch_prediction:list數據,結構為(batch_size,) #每個數據長度不一(在model參數mask存在的情況下) def f1_score_evaluation(batch_masks,batch_labels,batch_prediction):all_prediction = []all_labels = []batch_size = batch_masks.shape[0] #防止最后一batch的數據不夠batch_sizefor index in range(batch_size):#把沒有mask掉的原始tag都集合到一起length = sum(batch_masks[index].numpy()==1)_label = batch_labels[index].numpy().tolist()[:length]all_labels = all_labels+_label #把沒有mask掉的預測tag都集合到一起#_predict = y_pred[index][:length]all_prediction = all_prediction+y_pred[index]assert len(_label)==len(y_pred[index])assert len(all_prediction) == len(all_labels)score = f1_score(all_prediction,all_labels,average='weighted')return score#把每個batch的數據都驗證一遍,取均值 model.eval() #不啟用 BatchNormalization 和 Dropout,保證BN和dropout不發生變化 score_list = [] for step, batch_data in enumerate(test_loader):with torch.no_grad(): #這部分的代碼不用跟蹤反向梯度更新y_pred = model(sentence=batch_data['texts'],mask=batch_data['masks'])score = f1_score_evaluation(batch_masks=batch_data['masks'],batch_labels=batch_data['labels'],batch_prediction=y_pred)score_list.append(score) #score_list logger.info("average-f1-score:"+str(np.mean(score_list)))總結
以上是生活随笔為你收集整理的pytorch BiLSTM+CRF模型实现NER任务的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 微信小程序icon图标引入
- 下一篇: Yahoo中文