본문 바로가기

7. ML | DL | NLP

7/17(월) IT K-DT(90일차) / 10.BERT

 

 

10.BERT

BERT는 'Attention is all you need" 논문에서 볼 수 있는 Transformer 구조를 중점적으로 사용한 구조.

BERT는 transformer 구조를 사용하면서도 encoder 부분만 사용하여 학습을 진행함.

기존 모델은 대부분 encoder-decoder으로 이루어져 있으며,

GPT 또한 decoder 부분을 사용하여 text generation 문제를 해결하는 모델임.

Transformer 구조 역시 input에서 text의 표현을 학습하고,

decoder에서 우리가 원하는 task의 결과물을 만드는 방식으로 학습이 진행됨.

하지만, BERT는 decoder를 사용하지 않고 두 가지 대표적인 학습 방법으로 encoder를 학습시킨 후

특정 task의 fine-tuning을 활용하여 결과물을 얻는 방법으로 사용됨.

 

unlabeled data로부터 pre-train을 진행한 후, 특정 downstream task에 fine-tuning을 하는 구조.
deep bidrectional을 더욱 강조하여 기존의 모델들과의 차별성을 강조.
1개의 output layer만을 pre-trained BERT 모델에 추가하여

NLP의 다양한 주요 task(11개)에서 SOTA를 달성함.


GPT vs BERT

GPT와 BERT 모두 자연어 처리를 위한 딥러닝 기반 모델임.
그러나 GPT와 BERT는 다른 방식으로 동작하며 다른 목표를 가지고 있음.


아키텍처:
GPT: Decoder 구조만 사용함.
이 디코더는 이전 토큰들을 기반으로 다음 토큰을 생성하는 데 사용되며,
문맥을 이해하고 다음 단어를 예측하는 데 초점을 맞추고 있음.

BERT: Encoder 구조만 사용함.
이 모델은 Transformer의 인코더 레이어를 여러 층으로 쌓아 구성됨.
BERT는 양방향으로 텍스트를 인코딩하기 때문에 "Bidirectional"이라고도 함.


사전 훈련 방식:
GPT: 비지도 학습 방식으로 사전 훈련.
큰 규모의 텍스트 코퍼스를 사용하여 언어 모델을 사전 훈련하며, 이후에 해당 모델을 다른 NLP 작업에 적용할 수 있음.

BERT: MLM 및 NSP와 같은 사전 훈련 작업을 수행.
MLM은 입력 문장에서 임의의 토큰을 마스킹하고, 모델은 해당 토큰을 예측하도록 학습함.
NSP는 두 문장이 문맥적으로 연결되었는지 여부를 예측하는 작업.


문맥 이해 방식:

GPT: 이전에 나온 토큰들을 통해 다음 토큰을 예측하고 생성함으로써 문맥을 이해함.
이 모델은 텍스트의 좌측 방향으로만 참조함.

BERT: 입력 문장의 모든 토큰을 함께 참조하여 문맥을 이해함.
이 모델은 양방향적으로 문장을 인코딩하므로, 어떤 토큰이 주어진 문장에서 뒤에 나오는 토큰인지를 고려할 수 있음.


파인 튜닝:

GPT: 사전 훈련된모델을 토큰 또는 레이어를 추가하여 다른 NLP 작업에 맞게 조정할 수 있음.

BERT: 사전 훈련된 모델을 레이어를 추가하거나 조정하여 다운스트림 NLP 작업에 맞게 파인 튜닝할 수 있음.


 


mono-directional vs bi-directional

mono-directional(단방향)과 bi-directional(양방향)은 주로 자연어 처리에서 사용되는 용어.
이 용어들은 모델이 문맥을 이해하거나 정보를 처리하는 방식을 나타냄.\

  1. Mono-directional(단방향):
    이 방식은 문장의 좌측에서 우측으로 진행하면서 순차적으로 문맥을 파악하는 것을 의미함. 
    이 모델은 이전에 나온 정보만을 참조하여 다음 단어나 토큰을 예측하거나 처리함.
    GPT(Generative Pre-trained Transformer)는 Mono-directional 모델의 예시임.
  2. Bi-directional(양방향):
    이 방식은 양쪽 방향으로 문맥을 파악할 수 있기 때문에 좌측과 우측의 문맥을 모두 이해할 수 있음.
    즉, 현재 위치의 단어나 토큰을 예측하거나 처리하기 위해 이전에 나온 정보와 다음에 나올 정보를 모두 참조함.
    BERT(Bidirectional Encoder Representations from Transformers)는 Bi-directional 모델의 예시임.

 

10-1. BERT 모델의 개요

Language Model의 pre-training 방법은 BERT 이전에도 많이 연구되고 있었고,

실제로 좋은 성능을 내고 있었음.
특히 문장 단위의 task에서 두각을 보였는데, 이러한 연구들은 두 문장의 관계를 전체적으로 분석하여

예측하는 것을 목표로 함.
또한, 문장 뿐만 아니라 토큰 단위의 task(개체명의 인식, Q&A 등)에서도 좋은 성능을 보임.

 

BERT는 사전 학습된 언어 모델로, 텍스트 기반의 Downstream 작업에 널리 사용됨.

새로운 작업(Downstream 작업)에 사전 학습된 벡터표현을 적용하는 방법:
1) 특징 추출 사용:

사전 학습된 신경망 모델에서 중간 또는 최종 레이어의 활성화 값을 특징으로 사용함.

이 방법은 이미지나 오디오와 같은 다른 유형의 데이터에서 사용될 수 있으며,

모델의 고급 특징을 활용하여 downstream 작업을 수행함. (예: ELMo)
2) 전이 학습(Fine-tuning):
사전 학습된 모델의 가중치를 초기화하고, 새로운 작업에 맞게 조정하여 재학습함.

이 방법은 사전 학습된 모델의 일부 또는 전체 아키텍처와 가중치를 그대로 가져와서 새로운 작업에 적용함.

모델이 이미 유용한 특징을 학습했기 때문에 적은 양의 데이터로도 효과적인 성능을 달성할 수 있음.

 

10-2. BERT 모델 구조

BERT 모델은 크게 Pre-training(사전 학습) 부분과 Fine-tuning(미세 조정) 부분 두 가지로 구성됨.


1) Pre-training(사전 학습) 부분:
Pre-training 부분에서는 BERT 모델이 사전 학습을 수행함. 

이 단계에서는 대규모의 비지도 텍스트 데이터를 사용하여 모델을 사전 학습함.

BERT 모델은 Transformer라는 네트워크 아키텍처를 기반으로 하며, 여러 계층으로 구성되어 있음.
Pre-training 단계에서 BERT 모델은 MLM과 NSP의 두 가지 학습 목표를 가지고 학습됨.

MLM은 입력 문장에서 임의로 단어를 마스킹하고, 해당 단어를 모델이 예측하도록 하는 과정이고,

NSP는 두 개의 문장이 주어졌을 때, 문장이 연속적인지 여부를 모델이 예측하도록 하는 과정임.

2) Fine-tuning(미세 조정) 부분:
Fine-tuning 부분에서는 Pre-training 단계에서 학습된 BERT 모델을 특정한 downstream 작업에 맞게 조정.

이 단계에서는 Pre-training 단계에서 학습한 가중치를 초기화한 후,

작은 규모의 task-specific 데이터셋을 사용하여 모델을 추가로 학습시킴.
Fine-tuning 단계에서는 BERT 모델의 출력을 사용하여 다양한 downstream 작업을 수행할 수 있음.

예를 들어, 문장 분류, 개체명 인식, 질의 응답 등의 작업을 위해 BERT 모델의 출력을 분류기로 연결하거나 추가적인 네트워크를 구성하여 학습함.

Fine-tuning 과정에서는 task-specific 데이터셋에 대해 모델의 가중치를 업데이트하여 특정 작업에 최적화된 모델을 얻을 수 있음.

 

10-3. BERT 모델 입/출력

BERT는 학습을 위해 기존 transformer의 input 구조를 사용하면서도 WorldPiece 방법의 Tokenization을 사용.

 

출처: https://hwiyong.tistory.com/392

 

위 그림처럼 세 가지 임베딩(Token, Segment, Position)을 사용해서 문장을 표현함.


Token Embedding에서는 두 가지 특수 토큰(CLS, SEP)을 사용하여 문장을 구별함.

Special Classification token(CLS)은 모든 문장의 가장 첫 번째(문장의 시작) 토큰으로 삽입됨.

이 토큰은 Classification task에서는 사용되지만, 그렇지 않을 경우엔 무시됨.

 

Special Separator token(SEP)을 사용하여 첫 번째 문장과 두 번째 문장을 구별하며,

여기에 segment Embedding을 더해서 앞뒤 문장을 더욱 쉽게 구별할 수 있도록 도와줌.

이 토큰은 각 문장의 끝에 삽입됨.

Position Embedding은 transformer 구조에서도 사용된 방법으로 그림과 같이 각 토큰의 위치를 알려줌.


최종적으로 세 가지 임베딩을 더한 임베딩을 input으로 사용하게 됨.

 

10-4. BERT 모델의 사전학습(pre-training)

1) MLM(Masked Language Modeling): 
문장에서 단어 중의 일부를 [Mask] 토큰으로 바꾼 뒤, 가려진 단어를 예측하도록 학습함.

이 과정에서 BERT는 문맥을 파악하는 능력을 기르게 됨.

ex) 나는 하늘이 예쁘다고 생각한다 -> 나는 하늘이 [Mask] 생각한다.
ex) 나는 하늘이 예쁘다고 생각한다 -> 나는 하늘이 흐리다고 생각한다.
ex) 나는 하늘이 예쁘다고 생각한다 -> 나는 하늘이 예쁘다고 생각한다.

추가적으로 더욱 다양한 표현을 학습할 수 있도록 80%는 [Mask] 토큰으로 바꾸어 학습하지만, 

나머지 10%는 token을 random word로 바꾸고, 마지막 10%는 원본 word 그대로를 사용함.

 

2) NSP(Next Sentence Prediction): 
다음 문장이 올바른 문장인지 맞추는 문제. 이 문제를 통해 두 문장 사이의 관계를 학습함. 
문장 A와 B를 이어 붙이는데, 

B는 50% 확률로 관련 있는 문장(IsNext label) 또는 관련 없는 문장(NotNext label)을 사용.

10-5. BERT 모델 요약

ELMo: pre-training의 관점을 제시

→ GPT-1: transformer 구조에 적용해서 transformer가 pre-training에 효과적이라는 것을 밝힘

→ BERT: 양방향으로 개선

Deep Bidirectional Model을 통해 같은 pretraining 모델로 만든 모든 NLP Task에서 SOTA를 달성

  (SOTA: State-Of-The-Art. 어떤 분야에서 현재까지 가장 우수한 성능을 가진 모델 또는 방법)
pretraining 모델을 통해 적은 리소스로도 좋은 성능을 낼 수 있으나, 사전학습에 많은 시간과 비용이 필요함.

10-6. 답변 랭킹 모델 만들기 실습

데이터의 출처: https://github.com/songys/Chatbot_data
데이터의 위치: https://raw.githubusercontent.com/songys/Chatbot_data/master/ChatbotData.csv

import urllib.request
import pandas as pd

urllib.request.urlretrieve("https://raw.githubusercontent.com/songys/Chatbot_data/master/ChatbotData.csv", filename="ChatBotData.csv")

 

train_dataset = pd.read_csv('ChatBotData.csv')
print(len(train_dataset)) # train_dataset의 갯수

 

 

train_dataset

 

 

# 데이터셋 결측값 확인
# train_dataset.dropna().count()
train_dataset.replace('', float('NaN'), inplace=True)
print(train_dataset.isnull().values.any())

 

 

# 데이터셋 중복값 제거
# train_dataset = train_dataset.drop_duplicates()
train_dataset = train_dataset.drop_duplicates(['Q']).reset_index(drop=True)
print(len(train_dataset))

 

 

train_dataset = train_dataset.drop_duplicates(['A']).reset_index(drop=True)
print(len(train_dataset))

 

 

import matplotlib.pyplot as plt

question_list=list(train_dataset['Q'])
answer_list=list(train_dataset['A'])

# 데이터 분포 확인
# question_length=train_dataset['Q'].str.len()
# print(question_length.describe())

# 질문의 최대 길이
print('질문의 최대 길이 : ', max(len(question) for question in question_list))
# print('질문의 최대 길이 : ', question_length.max())

# 질문의 평균 길이
print('질문의 평균 길이 : ', sum(map(len, question_list))/len(question_list))
# print('질문의 평균 길이 : ', question_length.mean())

plt.hist([len(question) for question in question_list], bins=50)
plt.xlabel('length of samples')
plt.ylabel('number of samples')
plt.show()

 

 

 

# answer_length=train_dataset['A'].str.len()
# print(answer_length.describe())

# 답변의 최대 길이
print('답변의 최대 길이 : ', max(len(answer) for answer in answer_list))
# print('답변의 최대 길이 : ', answer_length.max())

# 답변의 평균 길이
print('답변의 평균 길이 : ', sum(map(len, answer_list))/len(answer_list))
# print('답변의 평균 길이 : ', answer_length.mean())

plt.hist([len(answer) for answer in answer_list], bins=50)
plt.xlabel('length of samples')
plt.ylabel('number of samples')
plt.show()

 

 



import random

print(f'question 개수: {len(question_list)}')
print(f'answer 개수: {len(answer_list)}')

 

 

response_candidates = random.sample(answer_list, 500)
response_candidates[:10]

 


한국어가 잘 적용되어있는 bert모델을 사용할 예정
koBERT-Transformer 모델 불러오기
(SKTBrain에서 공개한 한국어 데이터로 사전학습한 BERT 모델)
KoBERT : https://github.com/SKTBrain/KoBERT
KoBERT-Transformers : https://github.com/monologg/KoBERT-Transformers

!pip install kobert-transformers

import torch
from kobert_transformers import get_kobert_model, get_distilkobert_model

model = get_kobert_model()

model.eval() # 모델을 학습하지 않고 사용만 할 예정 # 실행모드

 

 

input_ids = torch.LongTensor([[31, 51, 99], [15, 5, 0]])  # 임의적으로 삽입한 인덱스 번호 6개
attention_mask = torch.LongTensor([[1,1,1],[1,1,0]]) # mask 6개
token_type_ids = torch.LongTensor([[0,0,1],[0,1,0]]) # type 6개
output = model(input_ids, attention_mask, token_type_ids)
output

 

 

output[0]

 

 

from kobert_transformers import get_tokenizer
tokenizer = get_tokenizer()

 

 

tokenizer.tokenize('[CLS] 한국어 모델을 공유합니다. [SEP]')

 

 

tokenizer.convert_tokens_to_ids(['[CLS]', '▁한국', '어', '▁모델', '을', '▁공유', '합니다', '.', '[SEP]'])

 

 

# 답변 모델 구성
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity

 

def get_cls_token(sentence):
  model.eval()
  tokenized_sent = tokenizer(
      sentence,
      return_tensors = 'pt',
      truncation=True,
      add_special_tokens=True,
      max_length=128
  )

  input_ids = tokenized_sent['input_ids']
  attention_mask = tokenized_sent['attention_mask']
  token_type_ids = tokenized_sent['token_type_ids']

  with torch.no_grad():
      output = model(input_ids, attention_mask, token_type_ids)

  cls_output = output[1]
  cls_token = cls_output.detach().cpu().numpy()

  return cls_token

 

def predict(query, candidates):
  candidates_cls = []

  for cand in candidates:
    cand_cls = get_cls_token(cand)
    candidates_cls.append(cand_cls)

  candidates_cls = np.array(candidates_cls).squeeze(axis=1) # squeeze:axis=1인 자원의 차원을 삭제

  query_cls = get_cls_token(query)
  similarity_list=  cosine_similarity(query_cls, candidates_cls)
  target_idx = np.argmax(similarity_list)
  return candidates[target_idx]

 

query = '너 요즘 바뻐?'
query_cls_hidden = get_cls_token(query)
print(query_cls_hidden)

 

sample_query = '너 요즘 바뻐?'
sample_candidates = ['바쁘면 가라', '아니 별로 안바뻐', '3인조 여성 그룹', '오늘은 여기까지']

predicted_answer = predict(query, sample_candidates)
print(f'결과: {predicted_answer}')

 

 

response_candidates = random.sample(answer_list, 100)

user_query = '나 오늘 헤어졌어'

predicted_answer = predict(user_query, sample_candidates)
print(f'결과: {predicted_answer}')

 

 

end = 1
while end == 1:
  sentence = input('하고싶은 말을 입력하세요 : ')
  if len(sentence) == 0:
    break
  predicted_answer = predict(sentence, response_candidates)
  print(f'결과: {predicted_answer}')
  print('\n')