BERT Word Embeddings 튜토리얼 번역 및 정리

2020. 2. 14. 13:59nlp

반응형

https://mccormickml.com/2019/05/14/BERT-word-embeddings-tutorial/

 

BERT Word Embeddings Tutorial · Chris McCormick

BERT Word Embeddings Tutorial 14 May 2019 By Chris McCormick and Nick Ryan In this post, I take an in-depth look at word embeddings produced by Google’s BERT and show you how to get started with BERT by producing your own word embeddings. This post is pres

mccormickml.com

 

기존 임베딩 vs BERT 임베딩

- 기존 임베딩은 문맥에 상관없이 같은 단어면 항상 같은 임베딩 

- BERT 임베딩은 문맥에 따라 달라짐 (동음이의어뿐만 아니라, 문맥 달라지면 같은 의미의 단어도 임베딩 달라짐)

 

Pre-trained BERT 불러오기

설치 : pip install pytorch-pretrained-bert

 

import torch
from pytorch_pretrained_bert import BertTokenizer, BertModel, BertForMaskedLM

tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')

 

BERT Input 포맷

- [CLS], [SEP]

- BERT vocab 안에 있는 토큰 

- BERT tokenizer의 토큰 ID

- mask ID  (뭐가 봐도 되는 토큰이고 아닌지)

- segment ID (문장 구분)

- positional embeddings (한 문장 내 특정 토큰의 순서)

 

두 문장을 입력하는 경우

[CLS] The man went to the store. [SEP] He bought a gallon of milk. [SEP]

 

한 문장을 입력하는 경우 

[CLS] The man went to the store. [SEP]

 

 

Tokenization

 

text = "Here is the sentence I want embeddings for."
marked_text = "[CLS]" + text + "[SEP]"

tokenized_text = tokenizer.tokenize(marked_text)
print(tokenized_text)

 

['[CLS]', 'here', 'is', 'the', 'sentence', 'i', 'want', 'em', '##bed', '##ding', '##s', 'for', '.', '[SEP]']

 

- original word가 subword로 쪼개짐 

- "##bed"는 어떤 단어의 일부, subword라는 뜻. 독립적인 단어 "bed"랑 다르다는 것을 보여주기 위해

- 전체 단어가 BERT vocab에 없으면 subword로 쪼갬 (vocab에서 찾을 때까지 쪼갬). 끝까지 못 찾으면 철자 하나씩까지 쪼갬

- 'OOV' : Out Of Vocabulary

- 'UNK' : UNKnown

 

text = "After stealing money from the bank vault, the bank robber was seen " \
       "fishing on the Mississippi river bank."
marked_text = "[CLS] " + text + " [SEP]"

tokenized_text = tokenizer.tokenize(marked_text)

indexed_text = tokenizer.convert_tokens_to_ids(tokenized_text)

for tup in zip(tokenized_text, indexed_text):
    print('{:<12} {:>6,}'.format(tup[0], tup[1]))
    

 

 

[CLS] 101

after 2,044

stealing 11,065

...

 

 

Segment ID

 

segments_ids = [1] * len(tokenized_text)

 

- 인풋 문장이 하나면 segment ID 모두 1로 주면 됨 (인풋 문장 + 마지막 [SEP] 까지 1 주기)

- 인풋 문장이 두개면 첫 문장은 0, 다음 문장은 1로 줘서 구분하기 

 

Extracting Embeddings

 

# Python list를 PyTorch tensor로 변환하기 
tokens_tensor = torch.tensor([indexed_tokens])
segments_tensors = torch.tensor([segments_ids])

# 미리 학습된 모델(가중치) 불러오기
model = BertModel.from_pretrained('bert-base-uncased')

# 모델 "evaluation" 모드 : feed-forward operation
model.eval()

 

# 우린 forward pass만 하지 [오차역전파해서 가중치 업데이트, 학습시키기] 이런거 안 하니까 no_grad()
# no_grad() 쓰면 계산 더 빠름 

# 각 레이어의 은닉상태 확인하기
with torch.no_grad():
    encoded_layers, _ = model(tokens_tensor, segment_tensors)

 

encoded_layers

1) layer 개수 : len(encoded_layers) # 12개

2) batch 개수 : len(encoded_layers[0]) # 1개

3) 단어/토큰 개수 :  len(encoded_layers[0][0]) # 22개

4) 은닉 상태 차원(hidden units/features) : len(encoded_layers[0][0][0]) # 768개

 

현재 encoded_layers의 차원 

[layer 개수, batch 개수, token 개수, feature 개수]

 

바꾸고 싶은 차원 

[token 개수, layer 개수, feature 개수]

 

- encoded_layers 자체는 Python list 

- 각 layer 안은 torch tensor로 이루어짐 

- encoded_layers[0].size()  # torch.Size([1,22,768])

 

 

12개의 layer를 합쳐서 하나의 큰 tensor로 만들기 

 

token_embeddings = torch.stack(encoded_layers, dim=0)

token_embeddings.size() # torch.Size([12, 1, 22, 768])

 

batch 차원 없애기

 

token_embeddings = torch.squeeze(token_embeddings, dim=1)

token_embeddings.size()  # torch.Size([12, 22, 768])

 

지금까지 하면 token_embeddings 차원은 

[layer 개수, token 개수, feature 개수]

여기서 layer 개수와 toke 개수의 자리만 바꾸면 됨 

 

token_embeddings = token_embeddings.permute(1,0,2)

token_embeddings.size()  # torch.Size([22, 12, 768])

 

은닉상태로부터 단어/문장 벡터 만들기

 

- 어느 레이어의 은닉상태가 가장 정확할까?

- 논문에서는 맨 마지막 4개의 레이어 합친게 성능 가장 좋았음 

 

1. 단어(토큰) 벡터 만들기

 

1) 맨 마지막 4개 레이어 이어붙이기(concatenate)

- 각 벡터의 길이는 4*768 = 3072 

- 4는 레이어 개수, 768은 feature 개수 

- 각 토큰을 3072 길이의 벡터로 나타냈는데, 총 22개의 토큰이 있음

 

token_vecs_cat = []

# token_embeddings : [22,12,768]
# token : [12,768]
for token in token_embeddings :
    cat_vec = torch.cat((token[-1], token[-2], token[-3], token[-4]), dim=0)
    
    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

 

2) 맨 마지막 4개 레이어 합치기(sum)

-  합치기만 했으니 한 토큰을 나타내는 벡터 길이는 여전히 768

- 토큰이 총 22개 있으니 최종 shape는 22*768

 

token_vecs_sum = []

for token in token_embeddings:
    
    sum_vec = torch.sum(token[-4:], dim=0)
    
    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

 

2. 문장 벡터 만들기

 

- 마지막 레이어에서 모든 토큰의 은닉상태 평균 구하기 

- 평균 구했으니 벡터 길이는 여전히 768

 

# encoded_layers : [12*1*22*768]
# token_vecs : [22*768]
token_vecs = encoded_layers[11][0]

# sentence_embedding : [768]
sentence_embedding = torch.mean(token_vecs, dim=0)

 

동음이의어 벡터 확인하기 

 

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 values for each meaning of "bank".

bank vault    tensor([ 2.1319, -2.1413, -1.6260,  0.8638,  3.3173])
bank robber   tensor([ 1.1868, -1.5298, -1.3770,  1.0648,  3.1446])
river bank    tensor([ 1.1295, -1.4725, -0.7296, -0.0901,  2.4970])

 

 

각 임베딩의 유사성 계산하기 

 

from scipy.spatial.distance import cosine

# 다른 의미의 bank 임베딩 비교 
# "bank robber" vs "river bank" (different meanings)
diff_bank = 1 - cosine(token_vecs_sum[10], token_vecs_sum[19])

# 같은 의미의 bank 임베딩 비교
# "bank robber" vs "bank vault" (same meaning)
same_bank = 1 - cosine(token_vecs_sum[10], token_vecs_sum[6])

print('Vector similarity for  *similar*  meanings:  %.2f' % same_bank)
print('Vector similarity for *different* meanings:  %.2f' % diff_bank)

 

Vector similarity for *similar* meanings: 0.95

Vector similarity for *different* meanings: 0.68

반응형