日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

结合原理与代码理解BiLSTM-CRF模型(pytorch)

發布時間:2023/12/31 编程问答 30 豆豆
生活随笔 收集整理的這篇文章主要介紹了 结合原理与代码理解BiLSTM-CRF模型(pytorch) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

前言

本文主要記錄學習使用BiLSTM-CRF模型來完成命名實體識別的過程中,對原理和代碼的理解。下面會通過推導模型原理,來解釋官方示例代碼(tutorial)。在學習原理的過程中主要參考了這兩篇博客:命名實體識別(NER):BiLSTM-CRF原理介紹+Pytorch_Tutorial代碼解析,其中有不少圖能幫助我們更好地理解模型;Bi-LSTM-CRF算法詳解-1,這篇里的公式推導比較簡單易懂。下面的解析會借鑒這兩篇博客中的內容,建議在往下看前先讀一下這兩篇了解原理。在BiLSTM-CRF模型中,我對LSTM模型這部分的理解還不夠深入,所以本文對它的介紹會少一些。


源代碼

首先貼上官方示例代碼,這段代碼實現了BiLSTM-CRF模型的訓練及預測,語料數據是作者隨便想的兩句話,最終實現了對語料中每個字進行實體標注。建議將代碼貼到IDE中,與之后的原理推導對照著看。

# Author: Robert Guthrieimport torch import torch.autograd as autograd import torch.nn as nn import torch.optim as optim device=torch.device('cuda:0')# 為CPU中設置種子,生成隨機數 torch.manual_seed(1)# 得到最大值的索引 def argmax(vec):# return the argmax as a python int_, idx = torch.max(vec, 1)return idx.item()def prepare_sequence(seq, to_ix):idxs = [to_ix[w] for w in seq]return torch.tensor(idxs, dtype=torch.long)# Compute log sum exp in a numerically stable way for the forward algorithm # 等同于torch.log(torch.sum(torch.exp(vec))),防止e的指數導致計算機上溢 def log_sum_exp(vec):max_score = vec[0, argmax(vec)]max_score_broadcast = max_score.view(1, -1).expand(1, vec.size()[1])return max_score + \torch.log(torch.sum(torch.exp(vec - max_score_broadcast)))class BiLSTM_CRF(nn.Module):def __init__(self, vocab_size, tag_to_ix, embedding_dim, hidden_dim):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.word_embeds = nn.Embedding(vocab_size, embedding_dim)self.lstm = nn.LSTM(embedding_dim, hidden_dim // 2,num_layers=1, bidirectional=True)# Maps the output of the LSTM into tag space.self.hidden2tag = nn.Linear(hidden_dim, self.tagset_size)# Matrix of transition parameters. Entry i,j is the score of# transitioning *to* i *from* j.# 轉移矩陣,transaction[i][j]表示從label_j轉移到label_i的概率,雖然是隨機生成的,但是后面會迭代更新self.transitions = nn.Parameter(torch.randn(self.tagset_size, self.tagset_size))# These two statements enforce the constraint that we never transfer# to the start tag and we never transfer from the stop tag# 設置任何標簽都不可能轉移到開始標簽。設置結束標簽不可能轉移到其他任何標簽self.transitions.data[tag_to_ix[START_TAG], :] = -10000self.transitions.data[:, tag_to_ix[STOP_TAG]] = -10000# 隨機初始化lstm的輸入(h_0,c_0)self.hidden = self.init_hidden()# 隨機生成輸入的h_0,c_0def init_hidden(self):return (torch.randn(2, 1, self.hidden_dim // 2),torch.randn(2, 1, self.hidden_dim // 2))def _forward_alg_new(self, feats):# Do the forward algorithm to compute the partition functioninit_alphas = torch.full([self.tagset_size], -10000.)# START_TAG has all of the score.init_alphas[self.tag_to_ix[START_TAG]] = 0.# Wrap in a variable so that we will get automatic backprop# Iterate through the sentenceforward_var_list = []forward_var_list.append(init_alphas)for feat_index in range(feats.shape[0]): # -1gamar_r_l = torch.stack([forward_var_list[feat_index]] * feats.shape[1])# gamar_r_l = torch.transpose(gamar_r_l,0,1)t_r1_k = torch.unsqueeze(feats[feat_index], 0).transpose(0, 1) # +1aa = gamar_r_l + t_r1_k + self.transitions# forward_var_list.append(log_add(aa))forward_var_list.append(torch.logsumexp(aa, dim=1))terminal_var = forward_var_list[-1] + self.transitions[self.tag_to_ix[STOP_TAG]]terminal_var = torch.unsqueeze(terminal_var, 0)alpha = torch.logsumexp(terminal_var, dim=1)[0]return alpha# 求所有可能路徑得分之和def _forward_alg(self, feats):# Do the forward algorithm to compute the partition function# 輸入:發射矩陣,實際就是LSTM的輸出————sentence的每個word經LSTM后,對應于每個label的得分# 輸出:所有可能路徑得分之和/歸一化因子/配分函數/Z(x)init_alphas = torch.full((1, self.tagset_size), -10000.)# START_TAG has all of the score.init_alphas[0][self.tag_to_ix[START_TAG]] = 0.# 包裝到一個變量里以便自動反向傳播# Wrap in a variable so that we will get automatic backpropforward_var = init_alphas# Iterate through the sentencefor feat in feats:alphas_t = [] # The forward tensors at this timestepfor next_tag in range(self.tagset_size):# 當前層這一點的發射得分要與上一層所有點的得分相加,為了用加快運算,將其擴充為相同維度的矩陣emit_score = feat[next_tag].view(1, -1).expand(1, self.tagset_size)# 前一層5個previous_tags到當前層當前tag_i的transition scorstrans_score = self.transitions[next_tag].view(1, -1)# 前一層所有點的總得分 + 前一節點標簽轉移到當前結點標簽的得分(邊得分) + 當前點的發射得分next_tag_var = forward_var + trans_score + emit_score# 求和,實現w_(t-1)到w_t的推進alphas_t.append(log_sum_exp(next_tag_var).view(1))# 保存的是當前層所有點的得分forward_var = torch.cat(alphas_t).view(1, -1)# 最后將最后一個單詞的forward var與轉移 stop tag的概率相加terminal_var = forward_var + self.transitions[self.tag_to_ix[STOP_TAG]]alpha = log_sum_exp(terminal_var)return alphadef _get_lstm_features(self, sentence):# 輸入:id化的自然語言序列# 輸出:序列中每個字符的Emission Scoreself.hidden = self.init_hidden()embeds = self.word_embeds(sentence).view(len(sentence), 1, -1)# lstm模型的輸出矩陣維度為(seq_len,batch,num_direction*hidden_dim)# 所以此時lstm_out的維度為(11,1,4)lstm_out, self.hidden = self.lstm(embeds, self.hidden)# 把batch維度去掉,以便接入全連接層lstm_out = lstm_out.view(len(sentence), self.hidden_dim)# 用一個全連接層將其轉換為(seq_len,tag_size)維度,才能生成最后的Emission Scorelstm_feats = self.hidden2tag(lstm_out)return lstm_featsdef _score_sentence(self, feats, tags):# Gives the score of a provided tag sequence# 輸入:feats——emission scores;tag——真實序列標注,以此確定轉移矩陣中選擇哪條路徑# 輸出:真是路徑得分score = torch.zeros(1)# 將START_TAG的標簽3拼接到tag序列最前面tags = torch.cat([torch.tensor([self.tag_to_ix[START_TAG]], dtype=torch.long), tags])# 路徑得分等于:前一點標簽轉移到當前點標簽的得分 + 當前點的發射得分for i, feat in enumerate(feats):score = score + \self.transitions[tags[i + 1], tags[i]] + feat[tags[i + 1]]# 最后加上STOP_TAG標簽的轉移得分,其發射得分為0,可以忽略score = score + self.transitions[self.tag_to_ix[STOP_TAG], tags[-1]]return scoredef _viterbi_decode(self, feats):# 預測路徑得分,維特比解碼,輸出得分與路徑值backpointers = []# Initialize the viterbi variables in log space# B:0 I:1 O:2 START_TAG:3 STOP_TAG:4init_vvars = torch.full((1, self.tagset_size), -10000.)# 維特比解碼的開始:一個START_TAG,得分設置為0,其他標簽的得分可設置比0小很多的數init_vvars[0][self.tag_to_ix[START_TAG]] = 0# forward_var表示當前這個字被標注為各個標簽的得分(概率)# forward_var at step i holds the viterbi variables for step i-1forward_var = init_vvars# 遍歷每個字,過程中取出這個字的發射得分for feat in feats:bptrs_t = [] # holds the backpointers for this stepviterbivars_t = [] # holds the viterbi variables for this step# 遍歷每個標簽,計算當前字被標注為當前標簽的得分for next_tag in range(self.tagset_size):# We don't include the emission scores here because the max# does not depend on them (we add them in below)# forward_var保存的是之前的最優路徑的值,然后加上轉移到當前標簽的得分,# 得到當前字被標注為當前標簽的得分(概率)next_tag_var = forward_var + self.transitions[next_tag]# 找出上一個字中的哪個標簽轉移到當前next_tag標簽的概率最大,并把它保存下載best_tag_id = argmax(next_tag_var)bptrs_t.append(best_tag_id)# 把最大的得分也保存下來viterbivars_t.append(next_tag_var[0][best_tag_id].view(1))# 然后加上各個節點的發射分數,形成新一層的得分# cat用于將list中的多個tensor變量拼接成一個tensorforward_var = (torch.cat(viterbivars_t) + feat).view(1, -1)# 得到了從上一字的標簽轉移到當前字的每個標簽的最優路徑# bptrs_t有5個元素backpointers.append(bptrs_t)# 其他標簽到結束標簽的轉移概率# Transition to STOP_TAGterminal_var = forward_var + self.transitions[self.tag_to_ix[STOP_TAG]]best_tag_id = argmax(terminal_var)# 最終的最優路徑得分path_score = terminal_var[0][best_tag_id]# Follow the back pointers to decode the best path.best_path = [best_tag_id]for bptrs_t in reversed(backpointers):best_tag_id = bptrs_t[best_tag_id]best_path.append(best_tag_id)# Pop off the start tag (we dont want to return that to the caller)# 無需返回最開始的start標簽start = best_path.pop()assert start == self.tag_to_ix[START_TAG] # Sanity check# 把從后向前的路徑正過來best_path.reverse()return path_score, best_path# 損失函數def neg_log_likelihood(self, sentence, tags):# len(s)*5feats = self._get_lstm_features(sentence)# 規范化因子 | 配分函數 | 所有路徑的得分之和forward_score = self._forward_alg_new(feats)# 正確路徑得分gold_score = self._score_sentence(feats, tags)# 已取反# 原本CRF是要最大化gold_score - forward_score,但深度學習一般都最小化損失函數,所以給該式子取反return forward_score - gold_score# 實際上是模型的預測函數,用來得到一個最佳的路徑以及路徑得分def forward(self, sentence): # dont confuse this with _forward_alg above.# 解碼過程,維特比解碼選擇最大概率的標注路徑# 先放入BiLstm模型中得到它的發射分數lstm_feats = self._get_lstm_features(sentence)# 然后使用維特比解碼得到最佳路徑score, tag_seq = self._viterbi_decode(lstm_feats)return score, tag_seqSTART_TAG = "<START>" STOP_TAG = "<STOP>" # 標簽一共有5個,所以embedding_dim為5 EMBEDDING_DIM = 5 # BILSTM隱藏層的特征數量,因為雙向所以是2倍 HIDDEN_DIM = 4# Make up some training data training_data = [("the wall street journal reported today that apple corporation made money".split(),"B I I I O O O B I O O".split() ), ("georgia tech is a university in georgia".split(),"B I O O O O B".split() )]word_to_ix = {} for sentence, tags in training_data:for word in sentence:if word not in word_to_ix:word_to_ix[word] = len(word_to_ix)tag_to_ix = {"B": 0, "I": 1, "O": 2, START_TAG: 3, STOP_TAG: 4}model = BiLSTM_CRF(len(word_to_ix), tag_to_ix, EMBEDDING_DIM, HIDDEN_DIM) optimizer = optim.SGD(model.parameters(), lr=0.01, weight_decay=1e-4)# Check predictions before training # 首先是用未訓練過的模型隨便預測一個結果 with torch.no_grad():precheck_sent = prepare_sequence(training_data[0][0], word_to_ix)precheck_tags = torch.tensor([tag_to_ix[t] for t in training_data[0][1]], dtype=torch.long)print(model(precheck_sent))# Make sure prepare_sequence from earlier in the LSTM section is loaded for epoch in range(300):for sentence, tags in training_data:# 訓練前將梯度清零optimizer.zero_grad()# 準備輸入sentence_in = prepare_sequence(sentence, word_to_ix)targets = torch.tensor([tag_to_ix[t] for t in tags], dtype=torch.long)# 前向傳播,計算損失函數loss = model.neg_log_likelihood(sentence_in, targets)# 反向傳播計算loss的梯度loss.backward()# 通過梯度來更新模型參數optimizer.step()# 使用訓練過的模型來預測一個序列,與之前形成對比 with torch.no_grad():precheck_sent = prepare_sequence(training_data[0][0], word_to_ix)print(model(precheck_sent))

模型簡介

BiLSTM-CRF模型是由雙向LSTM模型以及CRF模型組合而成,模型的輸入是字序列,輸出是模型給每個字預測的標簽,是一個標簽序列。

雙向LSTM模型用來生成發射矩陣,也就是每個字被標注為某個標簽的概率。其實我們用這個發射矩陣也可以進行命名實體識別,只需要從每個字被標注為各個標簽的概率中取最大的那個即可,但實際效果卻不是這么簡單的,BILSTM模型的發射矩陣沒有考慮標簽之間的約束關系,比如在BIO體系中,I不能在O之后出現。所以我們要對標簽的連接順序有所約束,這個約束將由CRF模型來生成。

CRF模型用來學習標簽之間的約束關系,最終生成一個轉移矩陣,可以理解為一個標簽后面連接另一個標簽的概率。

整個模型在預測時,會結合發射矩陣和轉移矩陣,使用維特比解碼算法來計算出得分最高的標注序列。下面就結合代碼分別對LSTM部分和CRF部分進行解釋說明。代碼的解釋順序可能與編寫順序不同。


LSTM模型

剛才說了LSTM模型的任務就是生成發射矩陣,在代碼中只涉及到BiLSTM_CRF類的初始化函數和_get_lstm_feature函數。

(1)BiLSTM_CRF類的初始化函數

在這個函數中完成了LSTM模型的初始參數設定。函數接受的參數有4個:vocab_size表示語料數據的詞表大小,tag_to_ix表示標簽的索引列表,embedding_dim表示輸入詞向量的維度,hidden_dim表示BILSTM模型中隱藏層狀態的維數。其中embedding_dim的值為5,hidden_dim的值為4。

def __init__(self, vocab_size, tag_to_ix, embedding_dim, hidden_dim):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.word_embeds = nn.Embedding(vocab_size, embedding_dim)self.lstm = nn.LSTM(embedding_dim, hidden_dim // 2,num_layers=1, bidirectional=True)self.hidden2tag = nn.Linear(hidden_dim, self.tagset_size)self.transitions = nn.Parameter(torch.randn(self.tagset_size, self.tagset_size))self.transitions.data[tag_to_ix[START_TAG], :] = -10000self.transitions.data[:, tag_to_ix[STOP_TAG]] = -10000self.hidden = self.init_hidden()

在函數體中,因為是雙向的,LSTM模塊的hidden_dim設置為2。self.hiden2tag表示一個全連接層,因為BILSTM模型輸出的維度為(seq_len,batch_size,hidden_dim),而我們想要的發射矩陣維度為(seq_len,tag_size),所以要用這個全連接層來將其維度進行變換。self.transitions表示CRF模型生成的轉移矩陣,self.transitions[i][j]表示j標簽后連接i標簽的得分,開始標簽之前沒有其他標簽,結束標簽之后也沒有其他標簽,所以要對這兩個初值進行設置。self.hidden表示LSTM模型的初始隱狀態。

(2)_get_lstm_feature函數

這個函數接收一個句子的子序列,輸出句子的發射矩陣。首先將句子通過Embedding模塊生成詞向量,再與隨機初始化的隱狀態一起輸入到lstm模型中,得到輸出矩陣后,通過全連接層將其變換到(seq_len,tag_size)維度,這樣就得到了發射矩陣。具體LSTM的工作過程可以參考這篇:pytorch中LSTM的細節分析理解。得到發射矩陣后,LSTM部分就結束了。


CRF模型

上面說了CRF模型的任務是生成一個轉移矩陣,首先介紹一下CRF模型的原理和公式:

選自李航老師的《統計學習方法》第11章

對于一個句子,其中每個字都有一個標簽,將這些字的標簽連接起來,就得到一個標簽序列,我們可以給一個句子標記出很多個不同的標簽序列。所以命名實體識別問題也可以看成是一個條件隨機場,輸入x為字序列,輸出y為對每個字標注的標簽序列。下圖是模型的公式表示:

CRF模型的關鍵點在于公式中的三個部分:、、,下面使用一張圖來解釋這三個部分。

假設有5個字的輸入序列(c0、c1、c2、c3、c4),各個字的標簽定義為(y0、y1、y2、y3、y4),有5種標簽(START、B、I、O、END)。那么每個字都有5種可能的標注,整個句子可能的標簽序列共有種,也就是圖中的黑色路徑。假設正確的標簽序列是(B、I、O、O、B),在圖中標記為紅色。下圖第一列為START,后面5列依次表示5個字的標注情況,最后一列表示END:

  • :整個句子存在的標簽序列共有種,每種序列在圖中都有一條唯一的路徑,每條路徑都設置一個得分,這個得分由路徑上每條邊與每個點的分數相加得到。所有路徑的得分相加就得到了,也叫配分函數或規范化因子。
  • :可以理解為圖中連接兩個圓圈的邊的得分,也就是圖中給出的序列標注轉移矩陣。:可以理解為體重每個圓圈店的得分,也就是圖中所給出的Emission Score。(說到這里,其實可以看出BiLSTM-CRF模型本質是一個CRF模型,只不過CRF模型中的,也就是發射矩陣,是單獨通過LSTM模型來生成的)

如果上面公式沒看懂也沒關系,到這兒只需要理解每個標簽序列是一個路徑,每個路徑都有一個得分,路徑得分=邊得分+點得分。

剛才說到我們的目標是通過CRF模型得到轉移矩陣,那么方法就是隨機初始化一個轉移矩陣,然后訓練CRF模型,在反向傳播過程中不斷調整轉移矩陣。既然是訓練,那么肯定會有一個損失函數,其實這里的損失函數就是上面圖中的11.10公式,但這個公式太復雜了,我們換一種寫法:

分子表示正確路徑的得分,分母表示所有路徑的得分。路徑得分用score()來計算,也就是上面說的點得分+邊得分。這個損失函數的分子和分母中都包含了指數運算,那么我們可以給式子兩邊取個對數,這樣即消除了分子的指數運算,又將除法化成了減法:

再回過頭來品一下這個式子:損失=真實路徑得分-所有路徑得分。隨著轉移矩陣的不斷調整,真實路徑得分會變得越來越大,損失函數也增大了,這有點不太符合反向傳播的工作機制。解決方法也很簡單,就是給式子兩端加個負號,在后面的代碼中可以體現出來。接下來就是CRF模型的重點:路徑得分


路徑得分

這個公式,無論如何也要搞清楚。其中表示序列中第個標簽的發射得分,表示序列中第個標簽的轉義得分,可以從發射矩陣和轉移矩陣中得到。就以上面那張圖中的標簽序列為例,序列=(B,I,O,O,B),,其中:

因為這里的序列是真實序列,所以在這里算出來的路徑得分也就是損失函數中的真實路徑得分。

def _score_sentence(self, feats, tags):score = torch.zeros(1)tags = torch.cat([torch.tensor([self.tag_to_ix[START_TAG]], dtype=torch.long), tags])# 路徑得分 = 前一點標簽轉移到當前點標簽的得分 + 當前點的發射得分for i, feat in enumerate(feats):score = score + \self.transitions[tags[i + 1], tags[i]] + feat[tags[i + 1]]# 最后加上STOP_TAG標簽的轉移得分,其發射得分為0,可以忽略score = score + self.transitions[self.tag_to_ix[STOP_TAG], tags[-1]]return score

代碼中的_score_sentence函數就用來計算真實路徑得分,輸入的是句子的發射矩陣feats和真實標簽序列tag,輸出的是真實路徑得分。cat函數將START標簽和真實標簽序列拼接在一起,enumerate函數用于遍歷發射矩陣,每次取出一個字的發射得分。有了上面的介紹,這部分代碼也就非常容易理解了。

然后就是CRF模型中的重中之重:所有路徑得分


所有路徑得分

現在我們已經明白了如何計算路徑得分,可以想到的是,要計算所有路徑得分,就用上面的方法把每條路徑都計算一遍。但實際上這種方法是非常低效的,例如計算(B,I,O,O,O)這個序列的得分時,計算過程如下:

與上面(B,I,O,O,B)序列得分的計算過程有許多重復的地方,而且標簽越多、輸入越長,重復的計算就越多。下圖可以直觀的看出這個問題,如果我們求出所有路徑的得分,然后再加起來,計算過程大致是這樣的:先算出紅色路徑的得分,再依次算出綠色、藍色、黃色和紫色路徑的得分。從圖中可以看出,這是一種深度優先的方法,過程中重復計算了很多次S列到c3列的得分。如果輸入長度為n,有m種標簽,那么就會有條路徑,每條路徑長度為n,可以計算出這種方法的時間復雜度為。

現在我們用一種新的方法來計算所有路徑得分,因為我們想要的是所有路徑得分,只要那個最后的總分,也就是圖中所有邊和所有點的累加和,對中間每條路徑的得分并不關心,那么我們可以把求這個總分的問題劃分成許多子問題。還是上圖這個例子,我們要計算所有路徑得分,也就是END點的分數,可以先計算出END點前一列,也就是c4這一列5個點的累積得分,再加上這一列的點與END點相連的邊的分數和END點的發射得分(0)即可;想要計算c4這一列所有點的累積得分,可以先計算出c3這一列所有點的累積得分,再加上兩列之間所有邊的得分和c4列5個點的發射得分即可。這種方法利用了動態規劃的思想,將問題拆分為多個子問題,而且子問題之間是有關聯的,后面的計算利用了前面的計算結果,我們把這種方法成為分數累積。現在我們已經清楚了分數累積的計算過程,先計算出c0列5個點的累積得分,然后求出c1列5個點的累積的分,最后推到END列,就得到了所有路徑得分。下面用一個簡單的例子來進行公式化推導:

假設輸入為(c0,c1,c2),標簽只有兩種(),發射得分用表示,設置發射矩陣為:

轉移得分用來表示,設置轉移矩陣為:

接著從損失函數中找到我們的目標,表示每條路徑的得分:

在開始前定義兩個變量:previous表示前一列的點的累積分數,now表示當前列的點的發射得分。然后開始推導:

(1)從c0列開始,now就是當前c0的發射得分,前面也沒有其他矩陣,所以previous為空。

此時,我們的目標值為:

(2)c0->c1,此時now就是c1的發射得分,previous就是c0列的得分,這個時候因為發生了標簽的轉移,所以我們要用到轉移矩陣。

需要注意的是,從c0到c1,兩個點,兩種標簽,所以共有四種標簽的轉移方式:分別是c0(1)->c1(1)、c0(1)->c1(2)、c0(2)->c1(1)、c0(2)->c1(2)。在計算它們時,公式分別是:

、、、

分別計算的話肯定費時,在這里我們可以將now和previous擴展成矩陣,這樣可以使運算實現矩陣化:

? ?

然后我們將previous、obs、轉移得分加起來:

計算結果中第一列就表示c1被標記為第一個標簽的兩條路徑的得分,第二列就表示c1被標記為第二個標簽的兩條路徑的得分。將兩列分別整合起來就構成了c1列留給c2列的previous值:

此時我們的目標值為:

其實到這里,如果沒有c2,上面的式子就是最終結果了。只不過是將目標函數中的用真實路徑代替了:

?

現在應該對分數累積的過程有了大致的了解,下面只需要重復這個過程,就可以得到最后的結果。

(3)c0->c1->c2

? ?

下面是矩陣的擴展,以及相加化簡運算,這里我就不再一個個敲公式了:

直到這里,previous已經計算得到c2到兩個標簽的路徑得分了,然后用這個previous來計算最終結果:

最后一個式子也就是我們的目標值,因為整個輸入長度為3,有兩種標簽,所以有8條路徑,都包含在了結果中。

在代碼中,所有路徑得分由_forward_alg函數計算得到,該函數接收一個句子的發射矩陣,開始的init_alphas就是可以看做是第一個字的previous。遍歷每一個字,得到當前字的obs、previous、轉移得分,然后相加生成新的previous,不斷往后推導。如果能看懂上面的公式推導,下面的代碼結合注釋也很容易懂了。

def _forward_alg(self, feats):init_alphas = torch.full((1, self.tagset_size), -10000.)# START_TAG has all of the score.init_alphas[0][self.tag_to_ix[START_TAG]] = 0.# 包裝到一個變量里以便自動反向傳播forward_var = init_alphasfor feat in feats:alphas_t = [] # The forward tensors at this timestepfor next_tag in range(self.tagset_size):# 取出obs,擴充維度emit_score = feat[next_tag].view(1, -1).expand(1, self.tagset_size)# 前一層5個previous_tags到當前層當前tag_i的transition scorstrans_score = self.transitions[next_tag].view(1, -1)# previous + 轉移得分 + obsnext_tag_var = forward_var + trans_score + emit_score# 求和,實現w_(t-1)到w_t的推進alphas_t.append(log_sum_exp(next_tag_var).view(1))# 生成新的previousforward_var = torch.cat(alphas_t).view(1, -1)# 最后將最后一個單詞的previous與轉移到stop tag的概率相加terminal_var = forward_var + self.transitions[self.tag_to_ix[STOP_TAG]]alpha = log_sum_exp(terminal_var)return alpha

到這里,CRF層的原理已經結束了。然后就是計算損失函數,然后不斷訓練,優化轉移矩陣。代碼中的neg_log_likelihood函數就是損失函數,在明白原理后這個函數也不難理解。訓練過程也是神經網絡訓練的常規做法,也不難理解。在訓練好模型后,我們得到了發射矩陣和最優化的轉移矩陣,接下來就是預測。

對于一個長度為n的新句子,標簽有m中,依然是有種可能的標簽序列,這里可以借助上面用到的一張圖:

圖中的所有路徑都是一個標簽序列,現在我們已經知道了圖中每條邊和每個點的得分,預測就是從這么多條路徑中找到一條邊得分+點得分最大的路徑,方法就是維特比解碼。這個方法相對CRF模型的原理來說簡單了不少,可以參考這篇:如何通俗地講解 viterbi 算法?,如果能看懂這篇里的內容,那么結合我在代碼中寫的注釋,應該可以很容易理解這個過程。

總結

以上是生活随笔為你收集整理的结合原理与代码理解BiLSTM-CRF模型(pytorch)的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。