[转载]个人认为最好的BERT讲解博客(上)

写在前面:这是我转载Meng Lee大佬的文章,是我目前看过最棒的关于BERT的文章,由于篇幅较长,我分为上下两部分。
我的转载得到了他的同意。
原文链接:https://leemeng.tw/attack_on_bert_transfer_learning_in_nlp.html

这是他个人博客的链接:LeeMeng
如果大家对此文有什么想讨论的,也可以通过邮箱与我讨论:wenwen2019@iscas.ac.cn


這是一篇 BERT 科普文,帶你直觀理解並實際運用現在 NLP 領域的巨人之力。

如果你還有印象,在自然語言處理(NLP)與深度學習入門指南裡我使用了 LSTM 以及 Google 的語言代表模型 BERT 來分類中文假新聞。而最後因為 BERT 本身的強大,我不費吹灰之力就在該 Kaggle 競賽達到 85 % 的正確率,距離第一名 3 %,總排名前 30 %。


當初我是使用 TensorFlow 官方釋出的 BERT 進行 fine tuning,但使用方式並不是那麼直覺。最近適逢 PyTorch Hub 上架 BERT,李宏毅教授的機器學習課程也推出了 BERT 的教學影片,我認為現在正是你了解並實際運用 BERT 的最佳時機!

這篇文章會簡單介紹 BERT 並展示如何使用 BERT 做遷移學習(Transfer Learning)。我在文末也會提供一些有趣的研究及應用 ,讓你可以進一步探索變化快速的 NLP 世界。

如果你完全不熟 NLP 或是壓根子沒聽過什麼是 BERT,我強力建議你之後找時間(或是現在!)觀看李宏毅教授說明 ELMo、BERT 以及 GPT 等模型的影片,淺顯易懂:李宏毅BERT(在这里,我把原文的油管链接换成B站链接)

我接下來會花點篇幅闡述 BERT 的基礎概念。如果你已經十分熟悉 BERT 而且迫不及待想要馬上將 BERT 應用到自己的 NLP 任務上面,可以直接跳到用 BERT fine tune 下游任務一節。

BERT:理解上下文的語言代表模型

一個簡單的 convention,等等文中會穿插使用的:

  • 代表
  • representation
  • repr.
  • repr. 向量

指的都是一個可以用來代表某詞彙(在某個語境下)的多維連續向量(continuous vector)。

現在在 NLP 圈混的,應該沒有人會說自己不曉得 Transformer 的經典論文 Attention Is All You Need 以及其知名的自注意力機制(Self-attention mechanism)BERT 全名為 Bidirectional Encoder Representations from Transformers,是 Google 以無監督的方式利用大量無標註文本「煉成」的語言代表模型,其架構為 Transformer 中的 Encoder。

我在淺談神經機器翻譯 & 用 Transformer 英翻中一文已經鉅細靡遺地解說過所有 Transformer 的相關概念,這邊就不再贅述。

BERT 其實就是 Transformer 中的 Encoder,只是有很多層 ( 圖片來源

BERT 是傳統語言模型的一種變形,而語言模型(Language Model, LM)做的事情就是在給定一些詞彙的前提下, 去估計下一個詞彙出現的機率分佈。在讓 AI 給我們寫點金庸裡的 LSTM 也是一個語言模型 ,只是跟 BERT 差了很多個數量級。

給定前 t 個在字典裡的詞彙,語言模型要去估計第 t + 1 個詞彙的機率分佈 P

為何會想要訓練一個 LM?因為有種種好處:

  • 好處 1:無監督數據無限大。不像 ImageNet 還要找人標注數據,要訓練 LM 的話網路上所有文本都是你潛在的資料集(BERT 預訓練使用的數據集共有 33 個字,其中包含維基百科及 BooksCorpus
  • 好處 2:厲害的 LM 能夠學會語法結構、解讀語義甚至指代消解。透過特徵擷取或是 fine-tuning 能更有效率地訓練下游任務並提升其表現
  • 好處 3:減少處理不同 NLP 任務所需的 architecture engineering 成本

一般人很容易理解前兩點的好處,但事實上第三點的影響也十分深遠。以往為了解決不同的 NLP 任務,我們會為該任務設計一個最適合的神經網路架構並做訓練。以下是一些簡單例子:

一般會依照不同 NLP 任務的性質為其貼身打造特定的模型架構

在這篇文章裡頭我不會一一介紹上述模型的運作原理,在這邊只是想讓你了解不同的 NLP 任務通常需要不同的模型,而設計這些模型並測試其 performance 是非常耗費成本的(人力、時間、計算資源)。

如果有一個能直接處理各式 NLP 任務的通用架構該有多好?

隨著時代演進,不少人很自然地有了這樣子的想法,而 BERT 就是其中一個將此概念付諸實踐的例子。BERT 論文的作者們使用 Transfomer Encoder、大量文本以及兩個預訓練目標,事先訓練好一個可以套用到多個 NLP 任務的 BERT 模型,再以此為基礎 fine tune 多個下游任務。

這就是近來 NLP 領域非常流行的兩階段遷移學習:

  • 先以 LM Pretraining 的方式預先訓練出一個對自然語言有一定「理解」的通用模型
  • 再將該模型拿來做特徵擷取或是 fine tune 下游的(監督式)任務
兩階段遷移學習在 BERT 下的應用:使用預先訓練好的 BERT 對下游任務做 fine tuning

上面這個示意圖最重要的概念是預訓練步驟跟 fine-tuning 步驟所用的 BERT 是一模一樣的。當你學會使用 BERT 就能用同個架構訓練多種 NLP 任務,大大減少自己設計模型的 architecture engineering 成本,投資報酬率高到爆炸。

壞消息是,天下沒有白吃的午餐。

要訓練好一個有 1.1 億參數的 12 層 BERT-BASE 得用 16 個 TPU chips 跑上整整 4 天,花費 500 鎂;24 層的 BERT-LARGE 則有 3.4 億個參數,得用 64 個 TPU chips(約 7000 鎂)訓練。喔對,別忘了多次實驗得把這些成本乘上幾倍。最近也有 NLP 研究者呼籲大家把訓練好的模型開源釋出以減少重複訓練對環境造成的影響。

好消息是,BERT 作者們有開源釋出訓練好的模型,只要使用 TensorFlow 或是 PyTorch 將已訓練好的 BERT 載入,就能省去預訓練步驟的所有昂貴成本。好 BERT 不用嗎?

雖然一般來說我們只需要用訓練好的 BERT 做 fine-tuning,稍微瞭解預訓練步驟的內容能讓你直觀地理解它在做些什麼。

BERT 在預訓練時需要完成的兩個任務 ( 圖片來源

Google 在預訓練 BERT 時讓它同時進行兩個任務:

  • 克漏字填空(1953 年被提出的 Cloze task,學術點的說法是 Masked Language Model, MLM)
  • 判斷第 2 個句子在原始文本中是否跟第 1 個句子相接(Next Sentence Prediction, NSP)

對上通天文下知地理的鄉民們來說,要完成這兩個任務簡單到爆。只要稍微看一下前後文就能知道左邊克漏字任務的 [MASK] 裡頭該填 退了;而 醒醒吧 後面接 你沒有妹妹 也十分合情合理。

讓我們馬上載入 PyTorch Hub 上的 BERT 模型體驗看看。首先我們需要安裝一些簡單的函式庫:

(2019/10/07 更新:因應 HuggingFace 團隊最近將 GitHub 專案大翻新並更名成 transformers,本文已直接 import 該 repo 並使用新的方法調用 BERT。底下的程式碼將不再使用該團隊在 PyTorch Hub 上 host 的模型。感謝網友 Hsien 提醒)

%%bash
pip install transformers tqdm boto3 requests regex -q

接著載入中文 BERT 使用的 tokenizer:

import torch
from transformers import BertTokenizer
from IPython.display import clear_output

PRETRAINED_MODEL_NAME = "bert-base-chinese"  # 指定繁簡中文 BERT-BASE 預訓練模型

# 取得此預訓練模型所使用的 tokenizer
tokenizer = BertTokenizer.from_pretrained(PRETRAINED_MODEL_NAME)

clear_output()
print("PyTorch 版本:", torch.__version__)

PyTorch 版本: 1.3.1

為了讓你直觀了解 BERT 運作,本文使用包含繁體與簡體中文的預訓練模型。 你可以在 Hugging Face 團隊的 repo 裡看到所有可從 PyTorch Hub 載入的 BERT 預訓練模型。截至目前為止有以下模型可供使用:

  • bert-base-chinese
  • bert-base-uncased
  • bert-base-cased
  • bert-base-german-cased
  • bert-base-multilingual-uncased
  • bert-base-multilingual-cased
  • bert-large-cased
  • bert-large-uncased
  • bert-large-uncased-whole-word-masking
  • bert-large-cased-whole-word-masking

這些模型的參數都已經被訓練完成,而主要差別在於:

  • 預訓練步驟時用的文本語言
  • 有無分大小寫
  • 層數的不同
  • 預訓練時遮住 wordpieces 或是整個 word

除了本文使用的中文 BERT 以外,常被拿來應用與研究的是英文的 bert-base-cased 模型。

現在讓我們看看 tokenizer 裡頭的字典資訊:

vocab = tokenizer.vocab
print("字典大小:", len(vocab))

字典大小: 21128

如上所示,中文 BERT 的字典大小約有 2.1 萬個 tokens。沒記錯的話,英文 BERT 的字典則大約是 3 萬 tokens 左右。我們可以瞧瞧中文 BERT 字典裡頭紀錄的一些 tokens 以及其對應的索引:

import random
random_tokens = random.sample(list(vocab), 10)
random_ids = [vocab[t] for t in random_tokens]

print("{0:20}{1:15}".format("token", "index"))
print("-" * 25)
for t, id in zip(random_tokens, random_ids):
    print("{0:15}{1:10}".format(t, id))

在这里插入图片描述

BERT 使用當初 Google NMT 提出的 WordPiece Tokenization ,將本來的 words 拆成更小粒度的 wordpieces,有效處理不在字典裡頭的詞彙 。中文的話大致上就像是 character-level tokenization,而有 ## 前綴的 tokens 即為 wordpieces。

以詞彙 fragment 來說,其可以被拆成 frag##ment 兩個 pieces,而一個 word 也可以獨自形成一個 wordpiece。wordpieces 可以由蒐集大量文本並找出其中常見的 pattern 取得。

另外有趣的是ㄅㄆㄇㄈ也有被收錄:

indices = list(range(647, 657))
some_pairs = [(t, idx) for t, idx in vocab.items() if idx in indices]
for pair in some_pairs:
    print(pair)

在这里插入图片描述

讓我們利用中文 BERT 的 tokenizer 將一個中文句子斷詞看看:

text = "[CLS] 等到潮水 [MASK] 了,就知道誰沒穿褲子。"
tokens = tokenizer.tokenize(text)
ids = tokenizer.convert_tokens_to_ids(tokens)

print(text)
print(tokens[:10], '...')
print(ids[:10], '...')

在这里插入图片描述

除了一般的 wordpieces 以外,BERT 裡頭有 5 個特殊 tokens 各司其職:

  • [CLS]:在做分類任務時其最後一層的 repr. 會被視為整個輸入序列的 repr.
  • [SEP]:有兩個句子的文本會被串接成一個輸入序列,並在兩句之間插入這個 token 以做區隔
  • [UNK]:沒出現在 BERT 字典裡頭的字會被這個 token 取代
  • [PAD]:zero padding 遮罩,將長度不一的輸入序列補齊方便做 batch 運算
  • [MASK]:未知遮罩,僅在預訓練階段會用到

如上例所示,[CLS] 一般會被放在輸入序列的最前面,而 zero padding 在之前的 Transformer 文章裡已經有非常詳細的介紹[MASK] token 一般在 fine-tuning 或是 feature extraction 時不會用到,這邊只是為了展示預訓練階段的克漏字任務才使用的。

現在馬上讓我們看看給定上面有 [MASK] 的句子,BERT 會填入什麼字:

"""
這段程式碼載入已經訓練好的 masked 語言模型並對有 [MASK] 的句子做預測
"""
from transformers import BertForMaskedLM

# 除了 tokens 以外我們還需要辨別句子的 segment ids
tokens_tensor = torch.tensor([ids])  # (1, seq_len)
segments_tensors = torch.zeros_like(tokens_tensor)  # (1, seq_len)
maskedLM_model = BertForMaskedLM.from_pretrained(PRETRAINED_MODEL_NAME)
clear_output()

# 使用 masked LM 估計 [MASK] 位置所代表的實際 token 
maskedLM_model.eval()
with torch.no_grad():
    outputs = maskedLM_model(tokens_tensor, segments_tensors)
    predictions = outputs[0]
    # (1, seq_len, num_hidden_units)
del maskedLM_model

# 將 [MASK] 位置的機率分佈取 top k 最有可能的 tokens 出來
masked_index = 5
k = 3
probs, indices = torch.topk(torch.softmax(predictions[0, masked_index], -1), k)
predicted_tokens = tokenizer.convert_ids_to_tokens(indices.tolist())

# 顯示 top k 可能的字。一般我們就是取 top 1 當作預測值
print("輸入 tokens :", tokens[:10], '...')
print('-' * 50)
for i, (t, p) in enumerate(zip(predicted_tokens, probs), 1):
    tokens[masked_index] = t
    print("Top {} ({:2}%):{}".format(i, int(p.item() * 100), tokens[:10]), '...')

在这里插入图片描述

Google 在訓練中文 BERT 鐵定沒看批踢踢,還無法預測出我們最想要的那個 退 字。而最接近的 的出現機率只有 2%,但我會說以語言代表模型以及自然語言理解的角度來看這結果已經不差了。BERT 透過關注 這兩個字,從 2 萬多個 wordpieces 的可能性中選出 作為這個情境下 [MASK] token 的預測值 ,也還算說的過去。


這是 BertViz 視覺化 BERT 注意力的結果,我等等會列出安裝步驟讓你自己玩玩。值得一提的是,以上是第 8 層 Encoder block 中 Multi-head attention 裡頭某一個 head 的自注意力結果。並不是每個 head 都會關注在一樣的位置。透過 multi-head 自注意力機制,BERT 可以讓不同 heads 在不同的 representation subspaces 裡學會關注不同位置的不同 repr.。

學會填克漏字讓 BERT 更好地 model 每個詞彙在不同語境下該有的 repr.,而 NSP 任務則能幫助 BERT model 兩個句子之間的關係,這在問答系統 QA自然語言推論 NLI 或是後面我們會看到的假新聞分類任務都很有幫助。

這樣的 word repr. 就是近年十分盛行的 contextual word representation 概念。跟以往沒有蘊含上下文資訊的 Word2Vec、GloVe 等無語境的詞嵌入向量有很大的差異。用稍微學術一點的說法就是:

Contextual word repr. 讓同 word type 的 word token 在不同語境下有不同的表示方式;而傳統的詞向量無論上下文,都會讓同 type 的 word token 的 repr. 相同。

直覺上 contextual word representation 比較能反映人類語言的真實情況,畢竟同個詞彙的含義在不同情境下相異是再正常不過的事情。在不同語境下給同個詞彙相同的 word repr. 這件事情在近年的 NLP 領域裡頭顯得越來越不合理。

為了讓你加深印象,讓我再舉個具體的例子:

情境 1
胖虎叫大雄去買漫畫,回來慢了就打他。

情境 2

妹妹說胖虎是「胖子」,他聽了很不開心。

很明顯地,在這兩個情境裡頭「他」所代表的語義以及指稱的對象皆不同。如果仍使用沒蘊含上下文 / 語境資訊的詞向量,機器就會很難正確地「解讀」這兩個句子所蘊含的語義了。

現在讓我們跟隨這個 Colab 筆記本安裝 BERT 的視覺化工具 BertViz,看看 BERT 會怎麼處理這兩個情境:

# 安裝 BertViz
import sys
!test -d bertviz_repo || git clone https://github.com/jessevig/bertviz bertviz_repo
if not 'bertviz_repo' in sys.path:
  sys.path += ['bertviz_repo']

# import packages
from bertviz.pytorch_transformers_attn import BertModel, BertTokenizer
from bertviz.head_view import show

# 在 jupyter notebook 裡頭顯示 visualzation 的 helper
def call_html():
  import IPython
  display(IPython.core.display.HTML('''
        <script src="/static/components/requirejs/require.js"></script>
        <script>
          requirejs.config({
            paths: {
              base: '/static/base',
              "d3": "https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.8/d3.min",
              jquery: '//ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min',
            },
          });
        </script>
        '''))

clear_output()

Setup 以後就能非常輕鬆地將 BERT 內部的注意力機制視覺化出來:

# 記得我們是使用中文 BERT
model_type = 'bert'
bert_version = 'bert-base-chinese'

bertviz_model = BertModel.from_pretrained(bert_version)
bertviz_tokenizer = BertTokenizer.from_pretrained(bert_version)

# 情境 1 的句子
sentence_a = "胖虎叫大雄去買漫畫,"
sentence_b = "回來慢了就打他。"
call_html()
show(bertviz_model, model_type, bertviz_tokenizer, sentence_a, sentence_b)

# 注意:執行這段程式碼以後只會顯示下圖左側的結果。
# 為了方便你比較,我把情境 2 的結果也同時附上

這是 BERT 裡第 9 層 Encoder block 其中一個 head 的注意力結果。

圖中的線條代表該 head 在更新「他」(左側)的 repr. 時關注其他詞彙(右側)的注意力程度。越粗代表關注權重(attention weights)越高。很明顯地這個 head 具有一定的指代消解(Coreference Resolution)能力,能正確地關注「他」所指代的對象。

要處理指代消解需要對自然語言有不少理解,而 BERT 在沒有標注數據的情況下透過自注意力機制、深度雙向語言模型以及「閱讀」大量文本達到這樣的水準,是一件令人雀躍的事情。

當然 BERT 並不是第一個嘗試產生 contextual word repr. 的語言模型。在它之前最知名的例子有剛剛提到的 ELMo 以及 GPT

ELMo、GPT 以及 BERT 都透過訓練語言模型來獲得 contextual word representation

ELMo 利用獨立訓練的雙向兩層 LSTM 做語言模型並將中間得到的隱狀態向量串接當作每個詞彙的 contextual word repr.;GPT 則是使用 Transformer 的 Decoder 來訓練一個中規中矩,從左到右的單向語言模型。你可以參考我另一篇文章:直觀理解 GPT-2 語言模型並生成金庸武俠小說來深入了解 GPT 與 GPT-2。

BERT 跟它們的差異在於利用 MLM(即克漏字)的概念及 Transformer Encoder 的架構,擺脫以往語言模型只能從單個方向(由左到右或由右到左)估計下個詞彙出現機率的窘境,訓練出一個雙向的語言代表模型。這使得 BERT 輸出的每個 token 的 repr. Tn 都同時蘊含了前後文資訊,真正的雙向 representation。

跟以往模型相比,BERT 能更好地處理自然語言,在著名的問答任務 SQuAD2.0 也有卓越表現:

SQuAD 2.0 目前排行榜的前 5 名有 4 個有使用 BERT

我想我又犯了解說癖,這些東西你可能在看這篇文章之前就全懂了。但希望這些對 BERT 的 high level 介紹能幫助更多人直覺地理解 BERT 的強大之處以及為何值得學習它。

假如你仍然似懂非懂,只需記得:

BERT 是一個強大的語言代表模型,給它一段文本序列,它能回傳一段相同長度且蘊含上下文資訊的 word repr. 序列,對下游的 NLP 任務很有幫助。

有了這樣的概念以後,我們接下來要做的事情很簡單,就是將自己感興趣的 NLP 任務的文本丟入 BERT ,為文本裡頭的每個 token 取得有語境的 word repr.,並以此 repr. 進一步 fine tune 當前任務,取得更好的結果。


以上就是该博客的上半部分,下半部分主要是介绍用BERT做下游任务

[转载]个人认为最好的BERT讲解博客(下)

  • 6
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值