本文中,小猴子和大家一起學(xué)習(xí)如何預(yù)訓(xùn)練 BERT 模型來識別文本中每個(gè)單詞的實(shí)體。 在處理 NLP 問題時(shí),BERT 經(jīng)常作為一種機(jī)器學(xué)習(xí)模型出現(xiàn),我們可以依靠它的性能。事實(shí)上,它已經(jīng)對超過 2,500M 的單詞進(jìn)行了預(yù)訓(xùn)練,并且其從單詞序列中學(xué)習(xí)信息的雙向特性使其成為一個(gè)強(qiáng)大的模型。
小猴子之前寫過關(guān)于如何利用 BERT 進(jìn)行文本分類的文章:保姆級教程,用PyTorch和BERT進(jìn)行文本分類 ,在本文中,我們將更多地關(guān)注如何將 BERT 用于命名實(shí)體識別 (NER) 任務(wù)。
什么是NER? NER 是 NLP 中的一項(xiàng)基礎(chǔ)有很重要的任務(wù),正所謂流水的NLP,鐵打的NER。 NER(實(shí)體識別任務(wù))指的是識別出文本中的具有特定意義的短語或詞。實(shí)體可以是單個(gè)詞,甚至可以是指代同一類別的一組詞,通常包括人名、地名、組織名、機(jī)構(gòu)名和時(shí)間等。
例如,假設(shè)我們下面的句子,我們想從這個(gè)句子中提取有關(guān)地 名的信息。
NER 任務(wù)的第一步是檢測實(shí)體。這可以是指代同一類別的一個(gè)詞或一組詞。舉個(gè)例子:
'天安門'
由單個(gè)單詞組成的實(shí)體'北京天安門'
由兩個(gè)詞組成的實(shí)體,但它們指的是同一類別。為了確保 BERT 模型知道一個(gè)實(shí)體可以是單個(gè)詞或一組詞,那么我們需要通過所謂的Inside-Outside-Beginning (IOB) 標(biāo)記在訓(xùn)練數(shù)據(jù)上提供有關(guān)實(shí)體開始和結(jié)束的信息。
識別到實(shí)體后,NER 任務(wù)的下一步是對實(shí)體進(jìn)行分類。根據(jù)我們的用例,實(shí)體的類別可以是任何東西。以下是實(shí)體類別的示例:
人物 :云朵君,小明,小猴子,詹姆斯,吳恩達(dá)地點(diǎn) :北京,成都,上海,深圳,天府廣場組織機(jī)構(gòu) :北京大學(xué),華為,騰訊,華西醫(yī)院命名實(shí)體識別的數(shù)據(jù)標(biāo)注方式 NER是一種序列標(biāo)注問題,因此他們的數(shù)據(jù)標(biāo)注方式也遵照序列標(biāo)注問題的方式,主要是BIO和BIOES兩種。這里直接介紹BIOES:
O,即Other,表示其他,用于標(biāo)記無關(guān)字符 BERT 用于 NER 運(yùn)用 BERT 解決與 NLP 相關(guān)的任務(wù),是非常方便的。
如果你還不熟悉 BERT,我建議你在閱讀本文之前閱讀我之前關(guān)于使用 BERT 進(jìn)行文本分類的文章。在那里,詳細(xì)介紹了有關(guān) BERT 模型架構(gòu)、模型期望的輸入數(shù)據(jù)類型以及將從模型中獲得的輸出的信息。
BERT模型在文本分類和 NER 問題中的區(qū)別在于如何設(shè)置模型的輸出。對于文本分類問題,僅使用特殊 [CLS] token 的 Embedding 向量輸出。而 NER 任務(wù)中,需要使用所有 token 的 Embedding向量輸出,希望模型預(yù)測每個(gè) token 的實(shí)體,則通過使用所有token 的 E mbedding向量輸出。
關(guān)于數(shù)據(jù)集 在本文中使用的數(shù)據(jù)集是 CoNLL-2003 數(shù)據(jù)集,它是專門用于 NER 任務(wù)的數(shù)據(jù)集。你可以通過下面的鏈接下載 Kaggle 上的數(shù)據(jù)。
NER數(shù)據(jù)命名實(shí)體識別數(shù)據(jù)
import pandas as pd df = pd.read_csv('ner.csv' ) df.head()
如圖所示,有一個(gè)由文本和標(biāo)簽組成的數(shù)據(jù)框。標(biāo)簽對應(yīng)于文本中每個(gè)單詞的實(shí)體類別。
總共有9個(gè)實(shí)體類別,分別是:
tim
---> 時(shí)間指示器實(shí)體nat
---> 自然現(xiàn)象實(shí)體看一下數(shù)據(jù)集上可用的唯一標(biāo)簽:
# 根據(jù)空格拆分標(biāo)簽,并將它們轉(zhuǎn)換為列表 labels = [i.split() for i in df['labels' ].values.tolist()]# 檢查數(shù)據(jù)集中有多少標(biāo)簽 unique_labels = set()for lb in labels: [unique_labels.add(i) for i in lb if i not in unique_labels] print(unique_labels)
{'B-tim', 'B-art', 'I-art', 'O', 'I-gpe', 'I-per', 'I-nat', 'I-geo', 'B-eve', 'B-org', 'B-gpe', 'I-eve', 'B-per', 'I-tim', 'B-nat', 'B-geo', 'I-org'}
將每個(gè)標(biāo)簽映射到它的id表示,反之亦然:
labels_to_ids = {k: v for v, k in enumerate(sorted(unique_labels))} ids_to_labels = {v: k for v, k in enumerate(sorted(unique_labels))} print(labels_to_ids)
{'B-art': 0, 'B-eve': 1, 'B-geo': 2, 'B-gpe': 3, 'B-nat': 4, 'B-org': 5, 'B-per': 6, 'B-tim': 7, 'I-art': 8, 'I-eve': 9, 'I-geo': 10, 'I-gpe': 11, 'I-nat': 12, 'I-org': 13, 'I-per': 14, 'I-tim': 15, 'O': 16}
注意到,每個(gè)實(shí)體類別都以字母I
或開頭B
。這對應(yīng)于前面提到的 IOB 標(biāo)記。I
表示 Intermediate 以及 B
表示 Beginning ??匆幌孪旅娴木渥舆M(jìn)一步了解 IOB 標(biāo)記的概念。
'Kevin'有B-pers
標(biāo)簽,它是個(gè)人實(shí)體的開始 'Durant'有I-pers
標(biāo)簽,它是個(gè)人實(shí)體的延續(xù) 'Brooklyn'有B-org
標(biāo)簽,它是一個(gè)組織實(shí)體的開始 'Nets' 有I-org
標(biāo)簽,它是組織實(shí)體的延續(xù) 其他詞被分配O
標(biāo)簽,它們不屬于任何實(shí)體 數(shù)據(jù)預(yù)處理 在能夠使用 BERT 模型對 token 級別的實(shí)體進(jìn)行分類之前,需要先進(jìn)行數(shù)據(jù)預(yù)處理,包括兩部分:tokenization 和調(diào)整標(biāo)簽以匹配 tokenization。
Tokenization 使用 HuggingFace 的預(yù)訓(xùn)練 BERT 基礎(chǔ)模型中的類BertTokenizerFast
,可以輕松實(shí)現(xiàn) tokenization。
為了給你一個(gè)例子,BERT 標(biāo)記器是如何工作的,讓我們看一下我們數(shù)據(jù)集中的一個(gè)文本:
text = df['text' ].values.tolist() example = text[36 ] print(example)
'Prime Minister Geir Haarde has refused to resign or call for early elections.'
對上面的文本進(jìn)行標(biāo)記BertTokenizerFast
非常簡單:
from transformers import BertTokenizerFast tokenizer = BertTokenizerFast.from_pretrained('bert-base-cased' ) text_tokenized = tokenizer(example, padding='max_length' , max_length=512 , truncation=True , return_tensors='pt' )
從上面的BertTokenizerFast
類調(diào)用tokenizer
方法時(shí),提供了幾個(gè)參數(shù):
padding
: 用特殊的 [PAD] token將序列填充到指定的最大長度(BERT 模型的最大序列長度為 512)。truncation
: 這是一個(gè)布爾值。如果將該值設(shè)置為 True,則不會使用超過最大長度的token。return_tensors
:返回的張量類型,取決于我們使用的機(jī)器學(xué)習(xí)框架。由于我們使用的是 PyTorch,所以我們使用pt
。以下是標(biāo)記化過程的輸出:
print(text_tokenized)
上下滑動(dòng)查看更多
{'input_ids' : tensor([[ 101, 3460, 2110, 144, 6851, 1197, 11679, 2881, 1162, 1144, 3347, 1106, 13133, 1137, 1840, 1111, 1346, 3212, 119, 102, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ............................. 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]), 'token_type_ids' : tensor([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, .................................. 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]), 'attention_mask' : tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, .................................. 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]])}
從上面結(jié)果可見:從Tokenization輸出是一個(gè)字典,其中包含三個(gè)變量:
input_ids
:序列中標(biāo)記的 id 表示。在 BERT 中,101 為特殊 [CLS] token 保留的id,id 102 為特殊**[SEP]** token保留的id,id 0 為**[PAD]** token保留的id。token_type_ids
:標(biāo)識一個(gè)token所屬的序列。由于每個(gè)文本只有一個(gè)序列,因此token_type_ids
的所有值都將為 0。attention_mask
:標(biāo)識一個(gè)token是真正的 token 還是 padding 得到的token。如果它是一個(gè)真正的token,則該值為 1,如果它是一個(gè) [PAD] token,則該值為 0。綜上所述,可以使用'decode'
方法,從上面的'input_ids'
中將這些id
解碼回原始序列,如下所示:
print(tokenizer.decode(text_tokenized.input_ids[0 ]))
'[CLS] Prime Minister Geir Haarde has refused to resign or call for early elections. [SEP] [PAD] [PAD] [PAD] [PAD] ... [PAD]'
在實(shí)現(xiàn)decode
方法后,我們得到了原始序列,并且是添加了來自 BERT 的特殊標(biāo)記,例如序列開頭的 [CLS] token,序列末尾的 [SEP] token,以及為了滿足要求的最大長度 512 而設(shè)置的一堆 [PAD] token。
在 Tokenization 之后,需要進(jìn)行調(diào)整每個(gè) token 的標(biāo)簽。
Tokenization后調(diào)整標(biāo)簽 因?yàn)樾蛄械拈L度不再匹配原始標(biāo)簽的長度,因此這是在Tokenization之后需要做的一個(gè)非常重要的步驟。
BERT 分詞器在底層使用了所謂的 word-piece tokenizer,它是一個(gè)子詞分詞器。這意味著 BERT tokenizer 可能會將一個(gè)詞拆分為一個(gè)或多個(gè)有意義的子詞。
例如,還是使用上面的序列作為例子:
上面的序列總共有 13 個(gè)標(biāo)記,因此它也有 13 個(gè)標(biāo)簽。但是,在 BERT 標(biāo)記化之后,我們得到以下結(jié)果:
print(tokenizer.convert_ids_to_tokens(text_tokenized['input_ids' ][0 ]))
['[CLS]', 'Prime', 'Minister', 'G', '##ei', '##r', 'Ha', '##ard', '##e', 'has', 'refused', 'to', 'resign', 'or', 'call', 'for', 'early', 'elections', '.', '[SEP]', '[PAD]', '[PAD]', '[PAD]','[PAD]', ... , '[PAD]']
在Tokenization之后需要解決兩個(gè)問題:
添加來自 BERT 的特殊 token,例如 [CLS]、 [SEP] 和 [PAD] 詞片Tokenization將不常見的詞拆分為它們的子詞,例如上面示例中的' Geir '
和' Haarde '
。這種詞片Tokenization有助于 BERT 模型學(xué)習(xí)相關(guān)詞的語義。
而這種詞片Tokenization和 BERT 添加特殊token的結(jié)果是Tokenization后的序列長度不再匹配初始標(biāo)簽的長度。
從上面的例子來看,現(xiàn)在Tokenization后的序列中總共有 512 個(gè)token,而標(biāo)簽的長度仍然和以前一樣。此外,序列中的第一個(gè)token不再是單詞' Prime '
,而是新添加的**[CLS]** token,因此我們也需要調(diào)整標(biāo)簽,以達(dá)到一一對應(yīng)的結(jié)果。使其與標(biāo)記化后的序列具有相同的長度。
如何實(shí)現(xiàn)標(biāo)簽調(diào)整呢?我們可以利用word_ids
標(biāo)記化結(jié)果中的方法如下:
word_ids = text_tokenized.word_ids() print(tokenizer.convert_ids_to_tokens(text_tokenized['input_ids' ][0 ])) print(word_ids)
['[CLS]', 'Prime', 'Minister', 'G', '##ei', '##r', 'Ha', '##ard', '##e', 'has', 'refused', 'to', 'resign', 'or', 'call', 'for', 'early', 'elections', '.', '[SEP]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', ..., '[PAD]'] [None, 0, 1, 2, 2, 2, 3, 3, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, None, None, None, None, ..., None]
從上面可以看出,每個(gè)拆分的 token 共享相同的 word_ids
,其中來自 BERT 的特殊 token,例如 [CLS]、 [SEP] 和 [PAD] 都沒有特定word_ids
的,結(jié)果是None
。
通過這些 word_ids
,并使用以下兩種方法來調(diào)整標(biāo)簽的長度:
只為每個(gè)拆分token的第一個(gè)子詞提供一個(gè)標(biāo)簽。子詞的延續(xù)將簡單地用'-100'
作為標(biāo)簽。所有沒有word_ids
的token也將標(biāo)為 '-100'
。 在屬于同一 token 的所有子詞中提供相同的標(biāo)簽。所有沒有word_ids
的token都將標(biāo)為 '-100'
。 下面的函數(shù)演示上面定義。
def align_label_example (tokenized_input, labels) : word_ids = tokenized_input.word_ids() previous_word_idx = None label_ids = [] for word_idx in word_ids: if word_idx is None : label_ids.append(-100 ) elif word_idx != previous_word_idx: try : label_ids.append(labels_to_ids[labels[word_idx]]) except : label_ids.append(-100 ) else : label_ids.append(labels_to_ids[labels[word_idx]] if label_all_tokens else -100 ) previous_word_idx = word_idx return label_ids
如果要應(yīng)用第一種方法,設(shè)置label_all_tokens
為 False。如果要應(yīng)用第二種方法,設(shè)置label_all_tokens
為 True,如以下代碼所示:
設(shè)置label_all_tokens=True
label = labels[36 ] label_all_tokens = True new_label = align_label_example(text_tokenized, label) print(new_label) print(tokenizer.convert_ids_to_tokens(text_tokenized['input_ids' ][0 ]))
[-100, 16, 16, 6, 6, 6, 14, 14, 14, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, -100, -100, -100, -100, ..., -100] ['[CLS]', 'Prime', 'Minister', 'G', '##ei', '##r', 'Ha', '##ard', '##e', 'has', 'refused', 'to', 'resign', 'or', 'call', 'for', 'early', 'elections', '.', '[SEP]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', ..., '[PAD]']
設(shè)置label_all_tokens=False
label_all_tokens = False new_label = align_label_example(text_tokenized, label) print(new_label) print(tokenizer.convert_ids_to_tokens(text_tokenized['input_ids' ][0 ]))
[-100, 16, 16, 6, -100, -100, 14, -100, -100, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, -100, -100, ..., -100] ['[CLS]', 'Prime', 'Minister', 'G', '##ei', '##r', 'Ha', '##ard', '##e', 'has', 'refused', 'to', 'resign', 'or', 'call', 'for', 'early', 'elections', '.', '[SEP]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', ..., '[PAD]']
在本文的其余部分,我們將實(shí)現(xiàn)第一個(gè)方法,其中我們將只為每個(gè)token中的第一個(gè)子詞提供一個(gè)標(biāo)簽并設(shè)置label_all_tokens=False
。
Dataset類 在為 NER 任務(wù)訓(xùn)練 BERT 模型之前,需要?jiǎng)?chuàng)建一個(gè)Dataset類來批量生成和獲取數(shù)據(jù)。
import torchdef align_label (texts, labels) : # 首先tokenizer輸入文本 tokenized_inputs = tokenizer(texts, padding='max_length' , max_length=512 , truncation=True ) # 獲取word_ids word_ids = tokenized_inputs.word_ids() previous_word_idx = None label_ids = [] # 采用上述的第一中方法來調(diào)整標(biāo)簽,使得標(biāo)簽與輸入數(shù)據(jù)對其。 for word_idx in word_ids: # 如果token不在word_ids內(nèi),則用 “-100” 填充 if word_idx is None : label_ids.append(-100 ) # 如果token在word_ids內(nèi),且word_idx不為None,則從labels_to_ids獲取label id elif word_idx != previous_word_idx: try : label_ids.append(labels_to_ids[labels[word_idx]]) except : label_ids.append(-100 ) # 如果token在word_ids內(nèi),且word_idx為None else : try : label_ids.append(labels_to_ids[labels[word_idx]] if label_all_tokens else -100 ) except : label_ids.append(-100 ) previous_word_idx = word_idx return label_ids# 構(gòu)建自己的數(shù)據(jù)集類 class DataSequence (torch.utils.data.Dataset) : def __init__ (self, df) : # 根據(jù)空格拆分labels lb = [i.split() for i in df['labels' ].values.tolist()] # tokenizer 向量化文本 txt = df['text' ].values.tolist() self.texts = [tokenizer(str(i), padding='max_length' , max_length = 512 , truncation=True , return_tensors='pt' ) for i in txt] # 對齊標(biāo)簽 self.labels = [align_label(i,j) for i,j in zip(txt, lb)] def __len__ (self) : return len(self.labels) def get_batch_data (self, idx) : return self.texts[idx] def get_batch_labels (self, idx) : return torch.LongTensor(self.labels[idx]) def __getitem__ (self, idx) : batch_data = self.get_batch_data(idx) batch_labels = self.get_batch_labels(idx) return batch_data, batch_labels
在上面的代碼中,在函數(shù) __init__
中調(diào)用帶有 tokenizer
變量的 BertTokenizerFast
類來標(biāo)記輸入文本,并 align_label
在Tokenization之后調(diào)整標(biāo)簽。 接下來,我們將數(shù)據(jù)隨機(jī)拆分為訓(xùn)練集、驗(yàn)證集和測試集。由于數(shù)據(jù)總數(shù)為 47959,出于演示目的和加快訓(xùn)練過程,這里將只選取其中的 1000 個(gè)。當(dāng)然,你也可以將所有數(shù)據(jù)用于模型訓(xùn)練。
import numpy as np df = df[0 :1000 ] df_train, df_val, df_test = np.split(df.sample(frac=1 , random_state=42 ), [int(.8 * len(df)), int(.9 * len(df))])
構(gòu)建模型 在本文中,使用來自 HuggingFace 的預(yù)訓(xùn)練 BERT 基礎(chǔ)模型。既然我們要在token級別對文本進(jìn)行分類,那么需要使用 BertForTokenClassification
類。
BertForTokenClassification
類是一個(gè)包裝 BERT 模型并在 BERT 模型之上添加線性層的模型,將充當(dāng)token級分類器。
from transformers import BertForTokenClassificationclass BertModel (torch.nn.Module) : def __init__ (self) : super(BertModel, self).__init__() self.bert = BertForTokenClassification.from_pretrained( 'bert-base-cased' , num_labels=len(unique_labels)) def forward (self, input_id, mask, label) : output = self.bert(input_ids=input_id, attention_mask=mask, labels=label, return_dict=False ) return output
在上面的代碼中,首先實(shí)例化模型并將每個(gè)token分類器的輸出設(shè)置為等于我們數(shù)據(jù)集上唯一實(shí)體的數(shù)量(在我們的例子是 17)。
訓(xùn)練模型 這里使用標(biāo)準(zhǔn)的 PyTorch 訓(xùn)練循環(huán)訓(xùn)練 BERT 模型,如下所示:
上下滑動(dòng)查看更多源碼
def train_loop (model, df_train, df_val) : # 定義訓(xùn)練和驗(yàn)證集數(shù)據(jù) train_dataset = DataSequence(df_train) val_dataset = DataSequence(df_val) # 批量獲取訓(xùn)練和驗(yàn)證集數(shù)據(jù) train_dataloader = DataLoader(train_dataset, num_workers=4 , batch_size=1 , shuffle=True ) val_dataloader = DataLoader(val_dataset, num_workers=4 , batch_size=1 ) # 判斷是否使用GPU,如果有,盡量使用,可以加快訓(xùn)練速度 use_cuda = torch.cuda.is_available() device = torch.device('cuda' if use_cuda else 'cpu' ) # 定義優(yōu)化器 optimizer = SGD(model.parameters(), lr=LEARNING_RATE) if use_cuda: model = model.cuda() # 開始訓(xùn)練循環(huán) best_acc = 0 best_loss = 1000 for epoch_num in range(EPOCHS): total_acc_train = 0 total_loss_train = 0 # 訓(xùn)練模型 model.train() # 按批量循環(huán)訓(xùn)練模型 for train_data, train_label in tqdm(train_dataloader): # 從train_data中獲取mask和input_id train_label = train_label[0 ].to(device) mask = train_data['attention_mask' ][0 ].to(device) input_id = train_data['input_ids' ][0 ].to(device) # 梯度清零?。?/span> optimizer.zero_grad() # 輸入模型訓(xùn)練結(jié)果:損失及分類概率 loss, logits = model(input_id, mask, train_label) # 過濾掉特殊token及padding的token logits_clean = logits[0 ][train_label != -100 ] label_clean = train_label[train_label != -100 ] # 獲取最大概率值 predictions = logits_clean.argmax(dim=1 ) # 計(jì)算準(zhǔn)確率 acc = (predictions == label_clean).float().mean() total_acc_train += acc total_loss_train += loss.item() # 反向傳遞 loss.backward() # 參數(shù)更新 optimizer.step() # 模型評估 model.eval() total_acc_val = 0 total_loss_val = 0 for val_data, val_label in val_dataloader: # 批量獲取驗(yàn)證數(shù)據(jù) val_label = val_label[0 ].to(device) mask = val_data['attention_mask' ][0 ].to(device) input_id = val_data['input_ids' ][0 ].to(device) # 輸出模型預(yù)測結(jié)果 loss, logits = model(input_id, mask, val_label) # 清楚無效token對應(yīng)的結(jié)果 logits_clean = logits[0 ][val_label != -100 ] label_clean = val_label[val_label != -100 ] # 獲取概率值最大的預(yù)測 predictions = logits_clean.argmax(dim=1 ) # 計(jì)算精度 acc = (predictions == label_clean).float().mean() total_acc_val += acc total_loss_val += loss.item() val_accuracy = total_acc_val / len(df_val) val_loss = total_loss_val / len(df_val) print( f'''Epochs: {epoch_num + 1 } | Loss: {total_loss_train / len(df_train): .3 f} | Accuracy: {total_acc_train / len(df_train): .3 f} | Val_Loss: {total_loss_val / len(df_val): .3 f} | Accuracy: {total_acc_val / len(df_val): .3 f} ''' ) LEARNING_RATE = 1e-2 EPOCHS = 5 model = BertModel() train_loop(model, df_train, df_val)
在上面的訓(xùn)練循環(huán)中,只訓(xùn)練了 5 個(gè) epoch 的模型,然后使用 SGD 作為優(yōu)化器。使用BertForTokenClassification
類計(jì)算每個(gè)批次的損失。
注意,有一個(gè)重要的步驟!在訓(xùn)練循環(huán)的每個(gè) epoch 中,在模型預(yù)測之后,需要忽略所有以 '-100' 作為標(biāo)簽的token。
下面是我們訓(xùn)練 BERT 模型 5 個(gè) epoch 后的訓(xùn)練輸出示例:
當(dāng)你訓(xùn)練自己的 BERT 模型時(shí),你將看到的輸出可能會有所不同,因?yàn)橛?xùn)練過程中存在隨機(jī)性。
大家想想,如何提高我們模型的性能。例如在我們有一個(gè)數(shù)據(jù)不平衡問題,因?yàn)橛泻芏鄮в?O'標(biāo)簽的token??梢酝ㄟ^在訓(xùn)練過程中添加不同類的權(quán)重來改進(jìn)我們的模型。
此外還可以嘗試不同的優(yōu)化器,例如具有權(quán)重衰減正則化的 Adam 優(yōu)化器。
評估模型 現(xiàn)在已經(jīng)訓(xùn)練了的模型,接下來可以使用測試數(shù)據(jù)集來測試模型的性能。評估代碼與驗(yàn)證代碼類似,這里不做詳細(xì)注釋。
def evaluate (model, df_test) : # 定義測試數(shù)據(jù) test_dataset = DataSequence(df_test) # 批量獲取測試數(shù)據(jù) test_dataloader = DataLoader(test_dataset, num_workers=4 , batch_size=1 ) # 使用GPU use_cuda = torch.cuda.is_available() device = torch.device('cuda' if use_cuda else 'cpu' ) if use_cuda: model = model.cuda() total_acc_test = 0.0 for test_data, test_label in test_dataloader: test_label = test_label[0 ].to(device) mask = test_data['attention_mask' ][0 ].to(device) input_id = test_data['input_ids' ][0 ].to(device) loss, logits = model(input_id, mask, test_label.long()) logits_clean = logits[0 ][test_label != -100 ] label_clean = test_label[test_label != -100 ] predictions = logits_clean.argmax(dim=1 ) acc = (predictions == label_clean).float().mean() total_acc_test += acc val_accuracy = total_acc_test / len(df_test) print(f'Test Accuracy: {total_acc_test / len(df_test): .3 f} ' ) evaluate(model, df_test)
就本案例而言,經(jīng)過訓(xùn)練的模型在測試集上平均達(dá)到了 92.22% 的準(zhǔn)確率。根據(jù)不同的任務(wù)或評價(jià)標(biāo)準(zhǔn),可以選用 F1 分?jǐn)?shù)、精度或召回率。
或者可以使用經(jīng)過訓(xùn)練的模型來預(yù)測文本或句子中每個(gè)單詞的實(shí)體,代碼如下:
上下滑動(dòng)查看更多源碼
def align_word_ids (texts) : tokenized_inputs = tokenizer(texts, padding='max_length' , max_length=512 , truncation=True ) word_ids = tokenized_inputs.word_ids() previous_word_idx = None label_ids = [] for word_idx in word_ids: if word_idx is None : label_ids.append(-100 ) elif word_idx != previous_word_idx: try : label_ids.append(1 ) except : label_ids.append(-100 ) else : try : label_ids.append(1 if label_all_tokens else -100 ) except : label_ids.append(-100 ) previous_word_idx = word_idx return label_idsdef evaluate_one_text (model, sentence) : use_cuda = torch.cuda.is_available() device = torch.device('cuda' if use_cuda else 'cpu' ) if use_cuda: model = model.cuda() text = tokenizer(sentence, padding='max_length' , max_length = 512 , truncation=True , return_tensors='pt' ) mask = text['attention_mask' ][0 ].unsqueeze(0 ).to(device) input_id = text['input_ids' ][0 ].unsqueeze(0 ).to(device) label_ids = torch.Tensor(align_word_ids(sentence)).unsqueeze(0 ).to(device) logits = model(input_id, mask, None ) logits_clean = logits[0 ][label_ids != -100 ] predictions = logits_clean.argmax(dim=1 ).tolist() prediction_label = [ids_to_labels[i] for i in predictions] print(sentence) print(prediction_label) evaluate_one_text(model, 'Bill Gates is the founder of Microsoft' )
從結(jié)果看,我們的模型將能夠很好地預(yù)測陌生句子中每個(gè)單詞的實(shí)體。
結(jié)論 在本文中,我們?yōu)槊麑?shí)體識別 (NER) 任務(wù)構(gòu)建了 BERT 模型。并訓(xùn)練了 BERT 模型來預(yù)測token級別的自定義文本或自定義句子的 IOB token。