pytorch BiLSTM+CRF模型实现NER任务
本次實現(xiàn)BiLSTM+CRF模型的數(shù)據(jù)來源于DataFountain平臺上的“產(chǎn)品評論觀點提取”競賽,數(shù)據(jù)僅用來做模型練習使用,并未參與實際競賽評分。
?
競賽地址:產(chǎn)品評論觀點提取
1. 數(shù)據(jù)分析
數(shù)據(jù)分為測試集數(shù)據(jù)7528條,測試集數(shù)據(jù)(未統(tǒng)計)。
測試集數(shù)據(jù)共有四個屬性,分別是:ID號,文本內(nèi)容,BIO實體標簽,class分類
本次比賽的任務(wù)一共分為兩部分,第一部分是NER部分,采用BIO實體標簽作為訓(xùn)練參考,另一部分為文本分類,目前只做了NER部分,因此暫時只針對NER部分講解。
測試集數(shù)據(jù)具體如下:
?首先分析一下我們的text長度,對訓(xùn)練集中7525個樣本的text長度進行統(tǒng)計,可以得到一下直方圖:
從圖中可以看出,文本長度為20左右的數(shù)據(jù)量最大,總體來說文本長度都在100個字符以內(nèi),所以我們可以把要放入模型訓(xùn)練的固定文本長度MAX_LEN設(shè)置為100。
要做NER任務(wù),就需要把每個字符預(yù)測為某一類標簽,加上O標簽,我們的標簽一共有九類,可以查看一下訓(xùn)練集中除了O以外的八類標簽的頻次情況,就可以知道是否有某一類標簽的數(shù)據(jù)量過少,過少的話就有可能帶來此類標簽預(yù)測效果不好的結(jié)果。
從上圖的頻次直方圖可以看到,出現(xiàn)頻次最高的是'I-PRODUCT',共有6000多次,頻次最低的是'B-BANK',共有不到2000次?
不過總的來說差距不算太懸殊,tag的分布可以說是較為均勻的,問題不大。
2. BiLSTM+CRF模型
?對于NER任務(wù),較為常用的模型有HMM、CRF等機器學習方法,(Bi)LSTM+CRF,CNN+CRF,BERT+CRF,后續(xù)我會記錄一下BERT+CRF等等模型的實現(xiàn),首先從BiLSTM+CRF開始。
?(1)BiLSTM+CRF模型示意圖:
?模型的輸入為固定長度的文本,文本中的每個詞向量為Wi。經(jīng)過BiLSTM層訓(xùn)練后進入全連接層,就可以得到每個詞在每個tag位置的概率了,因為我們總共有九個tag,所以全連接層的輸出的最低維度就是長度為9的向量。最后經(jīng)過CRF層訓(xùn)練后,就可以輸出loss值;如果是預(yù)測的話,使用CRF層的decode方法,就可以得到每個詞具體預(yù)測的tag了。
(2)模型各層數(shù)據(jù)結(jié)構(gòu)的變化示意圖:
之所以畫有這個數(shù)據(jù)結(jié)構(gòu)變化流程,是因為我個人非常糾結(jié)在模型變化中的數(shù)據(jù)結(jié)構(gòu)變化(可能是我菜..),但是在參考網(wǎng)上別的資料的時候,基本上沒有見過有提供這類總結(jié)的,所以我就畫個圖,萬一有人和我一樣需要呢[doge]
上圖中,輸入的結(jié)構(gòu)是[batch_size, seq_len],batch_size就是數(shù)據(jù)集每個batch的大小了這個很簡單,seq_len是文本長度,一般文本長度都是固定的(短于固定長度的話就需要padding)。輸入數(shù)據(jù)實際上就是經(jīng)過詞轉(zhuǎn)index,再經(jīng)過padding過后的訓(xùn)練集/驗證集數(shù)據(jù)了。
這里有一點需要注意:輸入數(shù)據(jù)的結(jié)構(gòu)中,我們一般第一個維度都是batch_size,但是實際上pytorch的各層模型中,它們默認的數(shù)據(jù)輸入?yún)?shù)都是batch_first=False,因此后面就需要將數(shù)據(jù)轉(zhuǎn)換成[seq_len, batch_size]的結(jié)構(gòu)。
另外非常重要的是,由[batch_size,seq_len]轉(zhuǎn)[seq_len, batch_size],不要用tensor類的view方法,也不要用numpy中的reshape方法,這樣轉(zhuǎn)換的維度是不正確的(你可以試試),應(yīng)該要用tensor類的permute方法來轉(zhuǎn)換維度。
圖中中間部分的轉(zhuǎn)換流程就不講了,沒什么太多問題,最后經(jīng)過CRF層的時候,如果是進行訓(xùn)練,那么需要參數(shù)emissions和tags,輸出結(jié)果就是loss值,不太一樣的是這里的loss值應(yīng)該是進行了一個-log的操作,因此直接輸出的loss值就會變成復(fù)數(shù),為了能夠用常用的優(yōu)化器進行參數(shù)優(yōu)化,這里的loss值需要乘以一個-1;如果是進行預(yù)測,就需要調(diào)用decode方法。
3. 具體實現(xiàn)
模型使用pytorch實現(xiàn),jupyter notebook版本的完整代碼在github上:NLP-NER-models
整體實現(xiàn)和核心代碼如下:
(1)詞轉(zhuǎn)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, #產(chǎn)品實體"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: #轉(zhuǎn)換成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): #如果從數(shù)據(jù)集獲得的text和label長度不一致next#1. word轉(zhuǎn)index#1.1 text詞匯text_index = []text_index.append(start_index) #先加入開頭indexfor word in text:text_index.append(w2i.get(word, unk_index)) #將當前詞轉(zhuǎn)成詞典對應(yīng)index,或不認識標注UNK的indextext_index.append(end_index) #最后加個結(jié)尾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類型的轉(zhuǎn)成tensor類型,方便后期進行訓(xùn)練texts = torch.LongTensor(texts)labels = torch.LongTensor(labels)masks = torch.tensor(masks, dtype=torch.uint8)return texts,labels,masks#unk:未知詞 pad:填充 start:文本開頭 end:文本結(jié)束 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) #中間截至(主要用在有上下句的情況下)#將訓(xùn)練集的字符全部轉(zhuǎn)成index,并改成MAX_LEN長度 texts,labels,masks = text_tag_to_index(train_dataset)(2)pytorch BiLSTM+CRF模型設(shè)置
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#####中間層設(shè)置#embedding層self.word_embeds = nn.Embedding(vocab_size,embedding_dim,padding_idx=self.pad_idx) #轉(zhuǎn)詞向量#lstm層self.lstm = nn.LSTM(embedding_dim, hidden_dim//2, num_layers = 1, bidirectional = True)#LSTM的輸出對應(yīng)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中的數(shù)據(jù)量不夠原本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的輸出轉(zhuǎn)為tagset_size長度的向量組(即輸出了每個tag的可能性)lstm_feats = self.hidden2tag(lstm_out) #4. 全連接層到CRF層if tags is not None: #訓(xùn)練用 #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#創(chuàng)建模型和優(yōu)化器 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) #顯示模型基本參數(shù) modelEmbedding層將詞indx轉(zhuǎn)詞向量,torch.nn.Embedding方法應(yīng)該是采用隨機變量確定embedding向量的;
LSTM層,隱藏層節(jié)點數(shù)6,因是BiLSTM所以需要乘以2,即共12個hidden units;
全連接層,由BiLSTM輸出的向量轉(zhuǎn)入全連接層,輸出維度為tag個數(shù);
CRF層。
采用SGD梯度優(yōu)化方法進行參數(shù)優(yōu)化,learning rate設(shè)為0.01(沒經(jīng)過特別研究),weight_decay設(shè)為1e-4。
(3)訓(xùn)練
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值,梯度并更新權(quán)重參數(shù) loss.backward() #retain_graph=True) #反向傳播,計算當前梯度optimizer.step() #根據(jù)梯度更新網(wǎng)絡(luò)參數(shù)loss_list.append(loss)(4)驗證集進行驗證
因為這次使用的數(shù)據(jù)集沒有驗證集,所以在開始時把訓(xùn)練集按7:3分為訓(xùn)練集和驗證集,把分離開的驗證集進行測試,看最后的F1-Score值評分情況。
#batch_masks:tensor數(shù)據(jù),結(jié)構(gòu)為(batch_size,MAX_LEN) #batch_labels: tensor數(shù)據(jù),結(jié)構(gòu)為(batch_size,MAX_LEN) #batch_prediction:list數(shù)據(jù),結(jié)構(gòu)為(batch_size,) #每個數(shù)據(jù)長度不一(在model參數(shù)mask存在的情況下) def f1_score_evaluation(batch_masks,batch_labels,batch_prediction):all_prediction = []all_labels = []batch_size = batch_masks.shape[0] #防止最后一batch的數(shù)據(jù)不夠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掉的預(yù)測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的數(shù)據(jù)都驗證一遍,取均值 model.eval() #不啟用 BatchNormalization 和 Dropout,保證BN和dropout不發(fā)生變化 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)))總結(jié)
以上是生活随笔為你收集整理的pytorch BiLSTM+CRF模型实现NER任务的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 微信小程序icon图标引入
- 下一篇: Yahoo中文