日韩黑丝制服一区视频播放|日韩欧美人妻丝袜视频在线观看|九九影院一级蜜桃|亚洲中文在线导航|青草草视频在线观看|婷婷五月色伊人网站|日本一区二区在线|国产AV一二三四区毛片|正在播放久草视频|亚洲色图精品一区

分享

保姆級教程,用PyTorch和BERT進(jìn)行命名實(shí)體識別

 漢無為 2022-06-17 發(fā)布于湖北

圖片
本文中,小猴子和大家一起學(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:

  • B,即Begin,表示開始
  • I, 即Intermediate,表示中間
  • E,即End,表示結(jié)尾
  • S,即Single,表示單個(gè)字符
  • 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 的 Embedding向量輸出。

關(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í)體類別,分別是:

  • geo ---> 地理實(shí)體
  • org ---> 組織實(shí)體
  • per ---> 個(gè)人實(shí)體
  • gpe ---> 地緣政治實(shí)體
  • tim ---> 時(shí)間指示器實(shí)體
  • art ---> 工件實(shí)體
  • eve ---> 事件實(shí)體
  • nat ---> 自然現(xiàn)象實(shí)體
  • O ---> 該單詞不屬于任何實(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)。
  • max_length: 序列的最大長度。
  • 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]
  • 一些 token 被子詞分詞器分成子詞。

詞片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)簽的長度:

  1. 只為每個(gè)拆分token的第一個(gè)子詞提供一個(gè)標(biāo)簽。子詞的延續(xù)將簡單地用'-100'作為標(biāo)簽。所有沒有word_ids 的token也將標(biāo)為 '-100'。
  2. 在屬于同一 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ù)。

上下滑動(dòng)查看更多源碼

import torch
def 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 BertForTokenClassification
class 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): .3f} | 
                Accuracy: {total_acc_train / len(df_train): .3f} |
                Val_Loss: {total_loss_val / len(df_val): .3f} | 
                Accuracy: {total_acc_val / len(df_val): .3f}'''
)

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ì)注釋。

上下滑動(dòng)查看更多源碼

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): .3f}')

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_ids

def 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。

    本站是提供個(gè)人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約