乡下人产国偷v产偷v自拍,国产午夜片在线观看,婷婷成人亚洲综合国产麻豆,久久综合给合久久狠狠狠9

  • <output id="e9wm2"></output>
    <s id="e9wm2"><nobr id="e9wm2"><ins id="e9wm2"></ins></nobr></s>

    • 分享

      保姆級教程,用PyTorch和BERT進行命名實體識別

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

      圖片
      本文中,小猴子和大家一起學(xué)習(xí)如何預(yù)訓(xùn)練 BERT 模型來識別文本中每個單詞的實體。

      在處理 NLP 問題時,BERT 經(jīng)常作為一種機器學(xué)習(xí)模型出現(xiàn),我們可以依靠它的性能。事實上,它已經(jīng)對超過 2,500M 的單詞進行了預(yù)訓(xùn)練,并且其從單詞序列中學(xué)習(xí)信息的雙向特性使其成為一個強大的模型。

      小猴子之前寫過關(guān)于如何利用 BERT 進行文本分類的文章:保姆級教程,用PyTorch和BERT進行文本分類,在本文中,我們將更多地關(guān)注如何將 BERT 用于命名實體識別 (NER) 任務(wù)。

      什么是NER?

      NER 是 NLP 中的一項基礎(chǔ)有很重要的任務(wù),正所謂流水的NLP,鐵打的NER。NER(實體識別任務(wù))指的是識別出文本中的具有特定意義的短語或詞。實體可以是單個詞,甚至可以是指代同一類別的一組詞,通常包括人名、地名、組織名、機構(gòu)名和時間等。

      例如,假設(shè)我們下面的句子,我們想從這個句子中提取有關(guān)地名的信息。

      圖片

      NER 任務(wù)的第一步是檢測實體。這可以是指代同一類別的一個詞或一組詞。舉個例子:

      • '天安門' 由單個單詞組成的實體
      • '北京天安門'由兩個詞組成的實體,但它們指的是同一類別。

      為了確保 BERT 模型知道一個實體可以是單個詞或一組詞,那么我們需要通過所謂的Inside-Outside-Beginning (IOB)  標(biāo)記在訓(xùn)練數(shù)據(jù)上提供有關(guān)實體開始和結(jié)束的信息。

      識別到實體后,NER 任務(wù)的下一步是對實體進行分類。根據(jù)我們的用例,實體的類別可以是任何東西。以下是實體類別的示例:

      • 人物:云朵君,小明,小猴子,詹姆斯,吳恩達(dá)
      • 地點:北京,成都,上海,深圳,天府廣場
      • 組織機構(gòu):北京大學(xué),華為,騰訊,華西醫(yī)院

      命名實體識別的數(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,表示單個字符
      • O,即Other,表示其他,用于標(biāo)記無關(guān)字符

      BERT 用于 NER

      運用 BERT 解決與 NLP 相關(guān)的任務(wù),是非常方便的。

      圖片

      如果你還不熟悉 BERT,我建議你在閱讀本文之前閱讀我之前關(guān)于使用 BERT 進行文本分類的文章。在那里,詳細(xì)介紹了有關(guān) BERT 模型架構(gòu)、模型期望的輸入數(shù)據(jù)類型以及將從模型中獲得的輸出的信息。

      BERT模型在文本分類和 NER 問題中的區(qū)別在于如何設(shè)置模型的輸出。對于文本分類問題,僅使用特殊 [CLS] token 的 Embedding 向量輸出。而 NER 任務(wù)中,需要使用所有 token 的 Embedding向量輸出,希望模型預(yù)測每個 token 的實體,則通過使用所有token 的 Embedding向量輸出。

      關(guān)于數(shù)據(jù)集

      在本文中使用的數(shù)據(jù)集是 CoNLL-2003 數(shù)據(jù)集,它是專門用于 NER 任務(wù)的數(shù)據(jù)集。你可以通過下面的鏈接下載 Kaggle 上的數(shù)據(jù)。

      NER數(shù)據(jù)命名實體識別數(shù)據(jù)

      import pandas as pd
      df = pd.read_csv('ner.csv')
      df.head()
      圖片

      如圖所示,有一個由文本和標(biāo)簽組成的數(shù)據(jù)框。標(biāo)簽對應(yīng)于文本中每個單詞的實體類別。

      總共有9個實體類別,分別是:

      • geo ---> 地理實體
      • org ---> 組織實體
      • per ---> 個人實體
      • gpe ---> 地緣政治實體
      • tim ---> 時間指示器實體
      • art ---> 工件實體
      • eve ---> 事件實體
      • nat ---> 自然現(xiàn)象實體
      • O ---> 該單詞不屬于任何實體。

      看一下數(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'}

      將每個標(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}

      注意到,每個實體類別都以字母I或開頭B。這對應(yīng)于前面提到的 IOB 標(biāo)記。I 表示 Intermediate 以及 B 表示 Beginning。看一下下面的句子進一步了解 IOB 標(biāo)記的概念。

      圖片
      • 'Kevin'有B-pers標(biāo)簽,它是個人實體的開始
      • 'Durant'有I-pers標(biāo)簽,它是個人實體的延續(xù)
      • 'Brooklyn'有B-org標(biāo)簽,它是一個組織實體的開始
      • 'Nets' 有I-org標(biāo)簽,它是組織實體的延續(xù)
      • 其他詞被分配O標(biāo)簽,它們不屬于任何實體

      數(shù)據(jù)預(yù)處理

      在能夠使用 BERT 模型對 token 級別的實體進行分類之前,需要先進行數(shù)據(jù)預(yù)處理,包括兩部分:tokenization 和調(diào)整標(biāo)簽以匹配 tokenization。

      Tokenization

      使用 HuggingFace 的預(yù)訓(xùn)練 BERT 基礎(chǔ)模型中的類BertTokenizerFast,可以輕松實現(xiàn) tokenization。

      為了給你一個例子,BERT 標(biāo)記器是如何工作的,讓我們看一下我們數(shù)據(jù)集中的一個文本:

      text = df['text'].values.tolist()
      example = text[36]
      print(example)
      'Prime Minister Geir Haarde has refused to
      resign or call for early elections.'

      對上面的文本進行標(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ù):

      • padding: 用特殊的 [PAD] token將序列填充到指定的最大長度(BERT 模型的最大序列長度為 512)。
      • max_length: 序列的最大長度。
      • truncation: 這是一個布爾值。如果將該值設(shè)置為 True,則不會使用超過最大長度的token。
      • return_tensors:返回的張量類型,取決于我們使用的機器學(xué)習(xí)框架。由于我們使用的是 PyTorch,所以我們使用pt。

      以下是標(biāo)記化過程的輸出:

      print(text_tokenized)

      上下滑動查看更多

      {'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輸出是一個字典,其中包含三個變量:

      • 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)識一個token所屬的序列。由于每個文本只有一個序列,因此token_type_ids的所有值都將為 0。
      • attention_mask:標(biāo)識一個token是真正的 token 還是 padding 得到的token。如果它是一個真正的token,則該值為 1,如果它是一個 [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]'

      在實現(xiàn)decode方法后,我們得到了原始序列,并且是添加了來自 BERT 的特殊標(biāo)記,例如序列開頭的 [CLS] token,序列末尾的 [SEP] token,以及為了滿足要求的最大長度 512 而設(shè)置的一堆 [PAD] token。

      在 Tokenization 之后,需要進行調(diào)整每個 token 的標(biāo)簽。

      Tokenization后調(diào)整標(biāo)簽

      因為序列的長度不再匹配原始標(biāo)簽的長度,因此這是在Tokenization之后需要做的一個非常重要的步驟。

      BERT 分詞器在底層使用了所謂的 word-piece tokenizer,它是一個子詞分詞器。這意味著 BERT tokenizer 可能會將一個詞拆分為一個或多個有意義的子詞。

      例如,還是使用上面的序列作為例子:

      圖片

      上面的序列總共有 13 個標(biāo)記,因此它也有 13 個標(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之后需要解決兩個問題:

      • 添加來自 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 個token,而標(biāo)簽的長度仍然和以前一樣。此外,序列中的第一個token不再是單詞' Prime ',而是新添加的**[CLS]** token,因此我們也需要調(diào)整標(biāo)簽,以達(dá)到一一對應(yīng)的結(jié)果。使其與標(biāo)記化后的序列具有相同的長度。

      如何實現(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]

      從上面可以看出,每個拆分的 token 共享相同的 word_ids,其中來自 BERT 的特殊 token,例如 [CLS][SEP][PAD] 都沒有特定word_ids的,結(jié)果是None。

      通過這些 word_ids,并使用以下兩種方法來調(diào)整標(biāo)簽的長度:

      1. 只為每個拆分token的第一個子詞提供一個標(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]']

      在本文的其余部分,我們將實現(xiàn)第一個方法,其中我們將只為每個token中的第一個子詞提供一個標(biāo)簽并設(shè)置label_all_tokens=False。

      Dataset類

      在為 NER 任務(wù)訓(xùn)練 BERT 模型之前,需要創(chuàng)建一個Dataset類來批量生成和獲取數(shù)據(jù)。

      上下滑動查看更多源碼

      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ù)隨機拆分為訓(xùn)練集、驗證集和測試集。由于數(shù)據(jù)總數(shù)為 47959,出于演示目的和加快訓(xùn)練過程,這里將只選取其中的 1000 個。當(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級別對文本進行分類,那么需要使用 BertForTokenClassification 類。

      BertForTokenClassification 類是一個包裝 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

      在上面的代碼中,首先實例化模型并將每個token分類器的輸出設(shè)置為等于我們數(shù)據(jù)集上唯一實體的數(shù)量(在我們的例子是 17)。

      訓(xùn)練模型

      這里使用標(biāo)準(zhǔn)的 PyTorch 訓(xùn)練循環(huán)訓(xùn)練 BERT 模型,如下所示:

      上下滑動查看更多源碼

      def train_loop(model, df_train, df_val):
          # 定義訓(xùn)練和驗證集數(shù)據(jù)
          train_dataset = DataSequence(df_train)
          val_dataset = DataSequence(df_val)
          # 批量獲取訓(xù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)
                  # 梯度清零??!
                  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)
            # 計算準(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:
            # 批量獲取驗證數(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)          
                  # 計算精度
                  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 個 epoch 的模型,然后使用 SGD 作為優(yōu)化器。使用BertForTokenClassification 類計算每個批次的損失。

      注意,有一個重要的步驟!在訓(xùn)練循環(huán)的每個 epoch 中,在模型預(yù)測之后,需要忽略所有以 '-100' 作為標(biāo)簽的token。

      下面是我們訓(xùn)練 BERT 模型 5 個 epoch 后的訓(xùn)練輸出示例:

      圖片

      當(dāng)你訓(xùn)練自己的 BERT 模型時,你將看到的輸出可能會有所不同,因為訓(xùn)練過程中存在隨機性。

      大家想想,如何提高我們模型的性能。例如在我們有一個數(shù)據(jù)不平衡問題,因為有很多帶有'O'標(biāo)簽的token??梢酝ㄟ^在訓(xùn)練過程中添加不同類的權(quán)重來改進我們的模型。

      此外還可以嘗試不同的優(yōu)化器,例如具有權(quán)重衰減正則化的 Adam 優(yōu)化器。

      評估模型

      現(xiàn)在已經(jīng)訓(xùn)練了的模型,接下來可以使用測試數(shù)據(jù)集來測試模型的性能。評估代碼與驗證代碼類似,這里不做詳細(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): .3f}')

      evaluate(model, df_test)

      就本案例而言,經(jīng)過訓(xùn)練的模型在測試集上平均達(dá)到了 92.22% 的準(zhǔn)確率。根據(jù)不同的任務(wù)或評價標(biāo)準(zhǔn),可以選用 F1 分?jǐn)?shù)、精度或召回率。

      或者可以使用經(jīng)過訓(xùn)練的模型來預(yù)測文本或句子中每個單詞的實體,代碼如下:

      上下滑動查看更多源碼

      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ù)測陌生句子中每個單詞的實體。

      結(jié)論

      在本文中,我們?yōu)槊麑嶓w識別 (NER) 任務(wù)構(gòu)建了 BERT 模型。并訓(xùn)練了 BERT 模型來預(yù)測token級別的自定義文本或自定義句子的 IOB token。

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

        0條評論

        發(fā)表

        請遵守用戶 評論公約