BERT Word Embeddings Tutorial
本文譯自?BERT Word Emebddings Tutorial,我將其中部分內容進行了精簡。轉載請注明出處
1. Loading Pre-Trained BERT
通過 Hugging Face 安裝 BERT 的 PyTorch 接口,該庫還包含其它預訓練語言模型的接口,如 OpenAI 的 GPT 和 GPT-2
如果您在 Google Colab 上運行此代碼,每次重新連接時都必須安裝此庫
-
!pip install transformers
BERT 是由 Google 發布的預訓練模型,該模型使用 Wikipedia 和?Book Corpus?數據進行訓練(Book Corpus 是一個包含不同類型的 10000 + 本書的數據集)。Google 發布了一系列 BERT 的變體,但我們在這里使用的是兩種可用尺寸("base" 和 "large")中較小的一種,并且我們設置忽略單詞大小寫
transformers?提供了許多應用于不同任務的 BERT 模型。在這里,我們使用最基本的?BertModel,這個接口的輸出不針對任何特定任務,因此用它提取 embeddings 是個不錯的選擇
現在讓我們導入 PyTorch,預訓練 BERT 模型以及 BERT tokenizer
-
import torch
-
from transformers import BertTokenizer, BertModel
- ?
-
# OPTIONAL: if you want to have more information on what's happening, activate the logger as follows
-
import logging
-
# logging.basicConfig(level=logging.INFO)
- ?
-
import matplotlib.pyplot as plt
-
%matplotlib inline
- ?
-
# Load pre-trained model tokenizer (vocabulary)
-
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
2. Input Formatting
由于 BERT 是一個預訓練模型,需要輸入特定格式的數據,因此我們需要:
幸運的是,使用?tokenizer.encode_plus?這個函數可以幫我們處理好一切。但是,由于這只是使用 BERT 的介紹,因此我們將主要以手動方式執行這些步驟
有關?tokenizer.encode_plus?這個函數的使用示例,可以這篇文章
2.1 Special Tokens
BERT 可以將一個或兩個句子作為輸入。如果是兩個句子,則使用?[SEP]?將它們分隔,并且?[CLS]?標記總是出現在文本的開頭;如果是一個句子,也始終需要兩個標記,此時?[SEP]?表示句子的結束。舉個例子
2 個句子的輸入:
[CLS] The man went to the store. [SEP] He bought a gallon of milk.
1 個句子的輸入:
[CLS] The man went to the store. [SEP]
2.2 Tokenization
BERT 提供了?tokenize?方法,下面我們看看它是如何處理句子的
-
text = "Here is the sentence I want embeddings for."
-
marked_text = "[CLS] " + text + " [SEP]"
- ?
-
# Tokenize our sentence with the BERT tokenizer.
-
tokenized_text = tokenizer.tokenize(marked_text)
- ?
-
# Print out the tokens.
-
print (tokenized_text)
-
# ['[CLS]', 'here', 'is', 'the', 'sentence', 'i', 'want', 'em', '##bed', '##ding', '##s', 'for', '.', '[SEP]']
注意 "embeddings" 這個詞是如何表示的:['em', '##bed', '##ding', '##s']
原始單詞已被拆分為較小的子詞和字符。這些子詞中前面兩個##哈希符號表示該子詞或字符是較大字的一部分。因此,例如 '##bed' 和 'bed' 這兩個 token 不相同;第一個用于子詞 "bed" 出現在較大詞中時,第二個是獨立的 token
為什么會這樣?因為 BERT 的 tokenizer 是使用 WordPiece 模型創建的。這個模型貪婪地創建了一個固定大小的詞匯表,其中包含了最適合我們語言的固定數量的字符、子詞和單詞。由于我們 BERT 模型的 tokenizer 限制詞匯量為 30000,因此 WordPiece 模型生成的詞匯表包含所有英文字符以及該模型所訓練英語預料庫中找到的約 30000 個最常見的單詞和子詞。該詞匯表包含四類東西:
具體來說,tokenzier 首先檢查整個單詞是否在詞匯表中,如果不在,它會嘗試將單詞分解為詞匯表中最大可能的子詞,如果子詞也沒有,它就會將整個單詞分解為單個字符。所以我們至少可以將一個單詞分解為單子字符的集合。基于此,不在詞匯表中的單詞不會分配給 "UNK" 這種萬能的標記,而是分解為子詞和字符標記
因此,即使 "embeddings" 這個詞不在詞匯表中,我們也不會將這個詞視為未知詞匯,而是將其分為子詞 tokens ['em', '##bed', '##ding', '##s'],這將保留單詞的一些上下文含義。我們甚至可以平均這些子詞的嵌入向量以生成原始單詞的近似向量。有關 WordPeice 的更多信息,請參考原論文
下面是我們詞匯表中的一些示例
-
list(tokenizer.vocab.keys())[5000:5020]
-
['knight',
-
'lap',
-
'survey',
-
'ma',
-
'##ow',
-
'noise',
-
'billy',
-
'##ium',
-
'shooting',
-
'guide',
-
'bedroom',
-
'priest',
-
'resistance',
-
'motor',
-
'homes',
-
'sounded',
-
'giant',
-
'##mer',
-
'150',
-
'scenes']
將文本分解為標記后,我們必須將句子轉換為詞匯索引列表。從這開始,我們將使用下面的例句,其中兩個句子都包含 "bank" 這個詞,且它們的含義不同
-
# Define a new example sentence with multiple meanings of the word "bank"
-
text = "After stealing money from the bank vault, the bank robber was seen " \
-
"fishing on the Mississippi river bank."
- ?
-
# Add the special tokens.
-
marked_text = "[CLS] " + text + " [SEP]"
- ?
-
# Split the sentence into tokens.
-
tokenized_text = tokenizer.tokenize(marked_text)
- ?
-
# Map the token strings to their vocabulary indeces.
-
indexed_tokens = tokenizer.convert_tokens_to_ids(tokenized_text)
- ?
-
# Display the words with their indeces.
-
for tup in zip(tokenized_text, indexed_tokens):
-
print('{:<12} {:>6,}'.format(tup[0], tup[1]))
-
[CLS] 101
-
after 2,044
-
stealing 11,065
-
money 2,769
-
from 2,013
-
the 1,996
-
bank 2,924
-
vault 11,632
-
, 1,010
-
the 1,996
-
bank 2,924
-
robber 27,307
-
was 2,001
-
seen 2,464
-
fishing 5,645
-
on 2,006
-
the 1,996
-
mississippi 5,900
-
river 2,314
-
bank 2,924
-
. 1,012
-
[SEP] 102
2.3 Segment ID
BERT 希望用 0 和 1 區分兩個句子。也就是說,對于?tokenized_text?中的每個 token,我們必須指明它屬于哪個句子。如果是單句,只需要輸入一系列 1;如果是兩個句子,請將第一個句子中的每個單詞(包括 [SEP])指定為 0,第二個句子指定為 1
-
# Mark each of the 22 tokens as belonging to sentence "1".
-
segments_ids = [1] * len(tokenized_text)
3. Extracting Embeddings
3.1 Running BERT on our text
接下來,我們需要將數據轉換為 PyTorch tensor 類型
-
# Convert inputs to PyTorch tensors
-
tokens_tensor = torch.tensor([indexed_tokens])
-
segments_tensors = torch.tensor([segments_ids])
調用?from_pretrained?函數將從互聯網上獲取模型。當我們加載?bert-base-uncased?時,我們會在 logging 記錄中看到模型的定義。該模型是一個具有 12 層的深度神經網絡,解釋每層的功能不在本文的范圍內,您可以查看我博客之前的內容來學習相關信息
model.eval()?會使得我們的模型處于測試模式,而不是訓練模式。在測試模式下,模型將會關閉 dropout regularization
-
# Load pre-trained model (weights)
-
model = BertModel.from_pretrained('bert-base-uncased',
-
output_hidden_states = True, # Whether the model returns all hidden-states.
-
)
- ?
-
# Put the model in "evaluation" mode, meaning feed-forward operation.
-
model.eval()
接下來,讓我們把示例文本傳入模型,并獲取網絡的隱藏狀態
torch.no_grad()?告訴 PyTorch 在前向傳播的過程中不構造計算圖(因為我們不會在這里反向傳播),這有助于減少內存消耗并加快運行速度
-
# Run the text through BERT, and collect all of the hidden states produced
-
# from all 12 layers.
-
with torch.no_grad():
- ?
-
outputs = model(tokens_tensor, segments_tensors)
- ?
-
# Evaluating the model will return a different number of objects based on
-
# how it's configured in the `from_pretrained` call earlier. In this case,
-
# becase we set `output_hidden_states = True`, the third item will be the
-
# hidden states from all layers. See the documentation for more details:
-
# https://huggingface.co/transformers/model_doc/bert.html#bertmodel
-
hidden_states = outputs[2]
3.2 Understanding the Output
hidden_states?包含的信息有點復雜,該變量有四個維度,分別是:
ちょっと待って,13 層?前面不是說 BERT 只有 12 層嗎?因為最前面的一層是 Word Embedding 層,剩下的是 12 個 Encoder Layer
第二個維度(batch size)是一次向模型提交多個句子時使用的;不過,在這里我們只有一個句子
-
print ("Number of layers:", len(hidden_states), " (initial embeddings + 12 BERT layers)")
- ?
-
layer_i = 0
-
print ("Number of batches:", len(hidden_states[layer_i]))
- ?
-
batch_i = 0
-
print ("Number of tokens:", len(hidden_states[layer_i][batch_i]))
- ?
-
token_i = 0
-
print ("Number of hidden units:", len(hidden_states[layer_i][batch_i][token_i]))
-
Number of layers: 13 (initial embeddings + 12 BERT layers)
-
Number of batches: 1
-
Number of tokens: 22
-
Number of hidden units: 768
通過快速瀏覽指定 token 和網絡層的數值范圍,您會發現其中大部分值介于 [-2, 2],少數在 - 12 附近
-
# For the 5th token in our sentence, select its feature values from layer 5.
-
token_i = 5
-
layer_i = 5
-
vec = hidden_states[layer_i][batch_i][token_i]
- ?
-
# Plot the values as a histogram to show their distribution.
-
plt.figure(figsize=(10,10))
-
plt.hist(vec, bins=200)
-
plt.show()
按層對值進行分組是有意義的,但是為了使用,我們希望它按 token 進行分組
當前的維度:[layers, batchs, tokens, features]
期望的維度:[tokens, layers, features]
幸運的是,PyTorch 的?permute?函數可以輕松的重新排列維度。但是目前?hidden_states?第一個維度是 list,所以我們要先結合各層,使其成為一個 tensor
-
# Concatenate the tensors for all layers. We use `stack` here to
-
# create a new dimension in the tensor.
-
token_embeddings = torch.stack(hidden_states, dim=0)
- ?
-
token_embeddings.size()
-
# torch.Size([13, 1, 22, 768])
接著我們消掉 "batch" 維度,因為我們不需要它
-
# Remove dimension 1, the "batches".
-
token_embeddings = token_embeddings.squeeze(dim=1)
- ?
-
token_embeddings.size()
-
# torch.Size([13, 22, 768])
最后,我們使用?permute?函數來交換維度
-
# Swap dimensions 0 and 1.
-
token_embeddings = token_embeddings.permute(1,0,2)
- ?
-
token_embeddings.size()
-
# torch.Size([22, 13, 768])
3.3 Creating word and sentence vectors from hidden states
我們希望為每個詞獲取單獨的向量,或者為整個句子獲取單獨的向量。但是對于輸入的每個詞,我們有 13 個向量,每個向量的長度為 768。為了獲得單個向量,我們需要將一些層的向量組合起來。但是,哪個層或組合哪些層比較好?
Word Vectors
我們用兩種方式創建詞向量。第一種方式是拼接最后四層,則每個單詞的向量長度為?4*768=3072
-
# Stores the token vectors, with shape [22 x 3,072]
-
token_vecs_cat = []
- ?
-
# `token_embeddings` is a [22 x 12 x 768] tensor.
- ?
-
# For each token in the sentence...
-
for token in token_embeddings:
- ?
-
# `token` is a [12 x 768] tensor
- ?
-
# Concatenate the vectors (that is, append them together) from
-
# the last four layers.
-
# Each layer vector is 768 values, so `cat_vec` is length 3072.
-
cat_vec = torch.cat((token[-1], token[-2], token[-3], token[-4]), dim=0)
- ?
-
# Use `cat_vec` to represent `token`.
-
token_vecs_cat.append(cat_vec)
- ?
-
print ('Shape is: %d x %d' % (len(token_vecs_cat), len(token_vecs_cat[0])))
-
# Shape is: 22 x 3072
第二種方式是將最后四層相加
-
# Stores the token vectors, with shape [22 x 768]
-
token_vecs_sum = []
- ?
-
# `token_embeddings` is a [22 x 12 x 768] tensor.
- ?
-
# For each token in the sentence...
-
for token in token_embeddings:
- ?
-
# `token` is a [12 x 768] tensor
- ?
-
# Sum the vectors from the last four layers.
-
sum_vec = torch.sum(token[-4:], dim=0)
- ?
-
# Use `sum_vec` to represent `token`.
-
token_vecs_sum.append(sum_vec)
- ?
-
print ('Shape is: %d x %d' % (len(token_vecs_sum), len(token_vecs_sum[0])))
-
# Shape is: 22 x 768
Sentence Vectors
有很多種策略可以獲得一個句子的單個向量表示,其中一種簡單的方法是將倒數第 2 層所有 token 的向量求平均
-
# `hidden_states` has shape [13 x 1 x 22 x 768]
- ?
-
# `token_vecs` is a tensor with shape [22 x 768]
-
token_vecs = hidden_states[-2][0]
- ?
-
# Calculate the average of all 22 token vectors.
-
sentence_embedding = torch.mean(token_vecs, dim=0)
- ?
-
print("Our final sentence embedding vector of shape:", sentence_embedding.size())
-
# Our final sentence embedding vector of shape: torch.Size([768])
3.4 Confirming contextually dependent vectors
為了確認這些向量的值是上下文相關的,我們可以檢查一下例句中 "bank" 這個詞的向量
“After stealing money from the?bank?vault, the?bank?robber was seen fishing on the Mississippi river?bank.”
-
for i, token_str in enumerate(tokenized_text):
-
print(i, token_str)
-
0 [CLS]
-
1 after
-
2 stealing
-
3 money
-
4 from
-
5 the
-
6 bank
-
7 vault
-
8 ,
-
9 the
-
10 bank
-
11 robber
-
12 was
-
13 seen
-
14 fishing
-
15 on
-
16 the
-
17 mississippi
-
18 river
-
19 bank
-
20 .
-
21 [SEP]
在這個例子中,我們通過累加最后四層的單詞向量,然后打印出來進行比較
-
print('First 5 vector values for each instance of "bank".')
-
print('')
-
print("bank vault ", str(token_vecs_sum[6][:5]))
-
print("bank robber ", str(token_vecs_sum[10][:5]))
-
print("river bank ", str(token_vecs_sum[19][:5]))
-
First 5 vector values for each instance of "bank".
- ?
-
bank vault tensor([ 3.3596, -2.9805, -1.5421, 0.7065, ...])
-
bank robber tensor([ 2.7359, -2.5577, -1.3094, 0.6797, ...])
-
river bank tensor([ 1.5266, -0.8895, -0.5152, -0.9298, ...])
很明顯值不同,但是通過計算向量之間的余弦相似度可以更精確的進行比較
-
from scipy.spatial.distance import cosine
- ?
-
# Calculate the cosine similarity between the word bank
-
# in "bank robber" vs "bank vault" (same meaning).
-
same_bank = 1 - cosine(token_vecs_sum[10], token_vecs_sum[6])
- ?
-
# Calculate the cosine similarity between the word bank
-
# in "bank robber" vs "river bank" (different meanings).
-
diff_bank = 1 - cosine(token_vecs_sum[10], token_vecs_sum[19])
- ?
-
print('Vector similarity for *similar* meanings: %.2f' % same_bank) # 0.94
-
print('Vector similarity for *different* meanings: %.2f' % diff_bank) # 0.69
3.5 Pooling Strategy & Layer Choice
BERT Authors
BERT 作者通過將不同的向量組合作為輸入特征提供給 NER 任務,并觀察所得的 F1 分數
雖然最后四層拼接在此特定任務上產生了最佳結果,但許多其他方法效果也不差,通常建議針對特定應用測試不同版本,結果可能會有所不同
Han Xiao's BERT-as-service
肖涵在 Github 上創建了一個名為?bert-as-service?的開源項目,該項目旨在使用 BERT 為您的文本創建單詞嵌入。他嘗試了各種方法來組合這些嵌入,并在項目的?FAQ?頁面上分享了一些結論和基本原理
肖涵的觀點認為:
總結
以上是生活随笔為你收集整理的BERT Word Embeddings Tutorial的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Transformer-XL解读(论文
- 下一篇: 预训练模型transformers综合总