2020. 2. 14. 13:59ㆍnlp
https://mccormickml.com/2019/05/14/BERT-word-embeddings-tutorial/
기존 임베딩 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
'nlp' 카테고리의 다른 글
세종 구어 말뭉치(tei 포맷) csv로 변환하기 (0) | 2020.05.10 |
---|---|
넷플릭스/네이버 시놉시스 word2vec 시각화하기 (0) | 2020.02.22 |
BERT 파헤치기 Part 1-2 번역 및 정리 (0) | 2020.02.11 |
그림으로 보는 BERT 번역 및 정리 (0) | 2020.02.11 |
Seq2seq pay Attention to Self Attention Part 1-2 번역 및 정리 (0) | 2020.02.10 |