hyeonzzz's Tech Blog

[딥러닝 파이토치 교과서] 10장. 임베딩 -(2) 본문

Deep Learning/Pytorch

[딥러닝 파이토치 교과서] 10장. 임베딩 -(2)

hyeonzzz 2024. 6. 1. 19:17

10.3 한국어 임베딩

 

<사전 훈련된 버트 모델을 사용한 한국어 임베딩 구현>

1. 라이브러리 불러오기

  • 한국어를 위한 버트 토크나이저 'bert-base-multilingual-cased' 사용

2. 문장의 토크나이징

  • 토크나이징한 결과 쪼개진 단어들이 정확하지 않다.
  • 버트 토크나이저가 단어의 가장 작은 조각을 기준으로 쪼개도록 설계되었기 때문이다.
  • 따라서 KoBert 같은 국내에서 개발한 모델들을 이용하는 것도 좋다.

3. 모델을 훈련시킬 텍스트 정의

  • 버트는 문장이 바뀔 때마다 0에서 1로 바뀌고, 다시 문장이 바뀌면 1에서 0으로 바뀐다.
  • [0, 0, 1, 1, 1, 0, 0, 0] 이라는 결과가 있다면 3개의 문장으로 구성된 것이다.

4. 문장 인식 단위 지정

  • 하나의 문장으로 인식시키기 위해 33개의 토큰에 벡터 1을 부여한다.

5. 데이터를 텐서로 변환

6. 모델 생성

model = BertModel.from_pretrained('bert-base-multilingual-cased',
                                  output_hidden_states = True,)
  • from_pretrained : 사전 훈련된 모델을 내려받는다.
  • 'bert-base-multilingual-cased' : 12개의 계층으로 구성된 심층 신경망. 다국어에 대한 임베딩을 처리
  • output_hidden_states : 버트 모델에서 은닉 상태의 값을 가져오기 위해 사용

7. 모델 훈련

8. 모델의 은닉 상태 정보 확인

# 모델 훈련
with torch.no_grad(): # 모델을 평가할 때 기울기 사용 X
    outputs = model(tokens_tensor, segments_tensors)
    hidden_states = outputs[2] # 네트워크의 은닉 상태를 가져온다
계층 수: 13   (initial embeddings + 12 BERT layers)
배치 수: 1
토큰 수: 33
은닉층 유닛 수: 768

 

Hugging Face의 트랜스포머 라이브러리를 사용하는 BERT 모델의 경우, outputs는 기본적으로 다음 세 가지를 포함하는 튜플로 반환된다:

  1. last_hidden_state: 최종 레이어의 출력 (각 입력 토큰에 대한 은닉 상태)
  2. pooler_output: 분류 작업을 위해 풀링된 출력 (주로 [CLS] 토큰에 대응하는 은닉 상태)
  3. hidden_states: 모든 레이어의 은닉 상태 (옵션, 모델 초기화 시 output_hidden_states=True로 설정해야 함)

13개인 이유는 첫 번째 임베딩 계층이 포함되었기 때문이다.

 

(계층, 배치, 토큰, 은닉층 유닛) 차원 -> (토큰, 계층, 은닉층 유닛) 차원으로 변환한다.

 

print('은닉 상태의 유형: ', type(hidden_states))
print('각 계층에서의 텐서 형태: ', hidden_states[0].size())
은닉 상태의 유형:  <class 'tuple'>
각 계층에서의 텐서 형태:  torch.Size([1, 33, 768])
  • 은닉 상태는 튜플로 구성되어 있다.
  • 각 계층에서의 텐서는 [1, 33, 768] 형태를 갖는다. (배치, 토큰, 은닉층 유닛)

9. 텐서의 형태 변경

# 텐서의 형태 변경
token_embeddings = torch.stack(hidden_states, dim=0) # 각 계층의 텐서 결합은 stack을 사용
token_embeddings.size() # 최종 텐서의 형태를 출력
torch.Size([13, 1, 33, 768])

 

token_embeddings = torch.squeeze(token_embeddings, dim=1) # 배치 차원(1) 제거
token_embeddings.size() # 배치 차원 제거 후 최종 텐서의 형태를 출력
torch.Size([13, 33, 768])
  • 배치 차원이 제거되었다.

10. 텐서 차원 변경

# 텐서 차원 변경
token_embeddings = token_embeddings.permute(1,0,2)
token_embeddings.size()
torch.Size([33, 13, 768])
  • permute는 차원을 맞교환할 때 사용한다.
  • (계층, 토큰, 은닉층 유닛) -> (토큰, 계층, 은닉층 유닛) 

11. 각 단어에 대한 벡터 형태 확인

각 단어에 대해 BERT 모델의 마지막 4개 레이어에서 생성된 은닉 상태 벡터를 결합하여, 각 단어의 문맥 정보를 더 풍부하게 담고 있는 새로운 벡터를 생성한다. 이 과정은 각 단어에 대한 문맥을 보다 정교하게 분석하고자 할 때 유용할 수 있다.

# 각 단어에 대한 벡터 형태 확인
token_vecs_cat = []  # 형태가 [33 x (33 x 768)]인 벡터를 [33 x 25344]로 변경하여 저장

# token_embeddings는 [33 x 12 x 768] 형태의 텐서를 갖는다
for token in token_embeddings:
    # 마지막 4개의 레이어의 은닉 상태 벡터를 이어붙인다
    cat_vec = torch.cat((token[-1], token[-2], token[-3], token[-4]), dim=0)
    token_vecs_cat.append(cat_vec)

# 결과 벡터의 형태를 출력
print('형태는: %d x %d' % (len(token_vecs_cat), len(token_vecs_cat[0])))
형태는: 33 x 3072
  1. token_vecs_cat = []:
    • 각 단어에 대한 벡터를 저장할 리스트를 초기화합니다.
  2. for token in token_embeddings::
    • token_embeddings 텐서의 각 단어에 대해 반복합니다. token_embeddings는 [33 x 12 x 768] 형태를 갖습니다.
    • 여기서 33은 토큰, 12는 계층, 768은 은닉층 유닛입니다.
  3. cat_vec = torch.cat((token[-1], token[-2], token[-3], token[-4]), dim=0):
    • 각 단어의 마지막 4개 레이어의 은닉 상태 벡터를 이어붙입니다.
    • token[-1]은 마지막 레이어의 은닉 상태 벡터, token[-2]는 그 이전 레이어의 은닉 상태 벡터 등입니다.
    • torch.cat 함수는 지정된 차원(dim=0)을 따라 텐서를 이어붙입니다. 결과는 [4 x 768] 크기의 텐서가 됩니다.
  4. token_vecs_cat.append(cat_vec):
    • 이어붙인 벡터(cat_vec)를 token_vecs_cat 리스트에 추가합니다.
  5. print('형태는: %d x %d' % (len(token_vecs_cat), len(token_vecs_cat[0]))):
    • token_vecs_cat 리스트의 첫 번째 차원(토큰 수)과 두 번째 차원(이어붙인 벡터의 길이)을 출력합니다.
    • 결과는 형태는: 33 x 3072가 됩니다. [토큰 수 x 결합된 벡터의 길이]

12. 계층을 결합하여 최종 단어 벡터 생성

각 단어에 대해 BERT 모델의 마지막 4개 레이어에서 생성된 은닉 상태 벡터를 합산하여, 각 단어의 문맥 정보를 더 간결하게 담고 있는 벡터를 생성한다. 이 벡터는 각 단어를 768차원의 벡터로 표현하며, 문맥 정보를 포함하고 있다.

# 계층을 결합하여 최종 단어 벡터 생성
token_vecs_sum = [] # [33x768] 형태의 토큰을 벡터로 저장
for token in token_embeddings: # 'token_embeddings'는 [33x12x768] 형태의 토큰을 갖는다
    sum_vec = torch.sum(token[-4:], dim=0) # 마지막 4개 계층의 벡터를 합산
    token_vecs_sum.append(sum_vec) # sum_vec를 사용하여 토큰을 표현
print ('형태는: %d x %d' % (len(token_vecs_sum), len(token_vecs_sum[0])))
형태는: 33 x 768
  • token은 [12 x 768] 형태의 텐서입니다.
  • token[-4:]는 마지막 4개 레이어에 해당하는 [4 x 768] 크기의 텐서를 선택합니다.
  • torch.sum(token[-4:], dim=0)은 이 4개 레이어의 벡터를 차원 0을 따라 합산하여 [768] 크기의 벡터를 만듭니다.
  • 즉, 각 레이어의 은닉 상태 벡터의 요소들을 더합니다

13. 문장 벡터

이제 전체 문장에 대한 단일 벡터를 구해야 한다. 768 길이의 벡터를 생성하는 각 토큰의 두 번째에서 마지막 은닉 계층을 평균화하면 쉽게 구할 수 있다.

# 문장 벡터
token_vecs = hidden_states[-2][0] #[33x768]
sentence_embedding = torch.mean(token_vecs, dim=0)
print ("최종 임베딩 벡터의 형태:", sentence_embedding.size())
최종 임베딩 벡터의 형태: torch.Size([768])

 

14. 토큰과 인덱스 출력

15. 단어 벡터 확인

# 단어 벡터 확인
print("사과가 많았다", str(token_vecs_sum[6][:5]))
print("나에게 사과했다", str(token_vecs_sum[10][:5]))
print("사과를 먹었다", str(token_vecs_sum[19][:5]))
사과가 많았다 tensor([-0.5844, -4.0836,  0.4906,  0.8915, -1.8054])
나에게 사과했다 tensor([-0.8631, -3.4047, -0.7351,  0.9805, -2.6700])
사과를 먹었다 tensor([ 0.6756, -0.3618,  0.0586,  2.2050, -2.4193])

 

16. 코사인 유사도 계산

# 코사인 유사도 계산
from scipy.spatial.distance import cosine
diff_apple = 1 - cosine(token_vecs_sum[5], token_vecs_sum[27]) # '사과가 많았다'와 '나에게 사과했다'에서 단어 '사과' 사이의 코사인 유사도 계싼
same_apple = 1 - cosine(token_vecs_sum[5], token_vecs_sum[16]) # '사과가 많았다'와 '사과를 먹었다'에 있는 '사과'사이의 코사인 유사도를 계산
print('*유사한* 의미에 대한 벡터 유사성:  %.2f' % same_apple)
print('*다른* 의미에 대한 벡터 유사성:  %.2f' % diff_apple)
*유사한* 의미에 대한 벡터 유사성:  0.86
*다른* 의미에 대한 벡터 유사성:  0.91

 

  • 한국어에 대한 정확한 판별이 어렵다.
  • 사과라는 단어가 쪼개져 있기 때문에 정확한 결과라고 하기 어렵다.