2020년 8월 6일 목요일

OpenAI 기반 GPT3 와 BERT 설치, 미세조정(파인튜닝) 방법

이 글은 최근 큰 이슈가 되고 있는 자연어 처리 인공지능 모델인 OpenAI 기반 GPT3, BERT 설치 사용 방법을 간략히 나눔해 본다.

GPT3 installation

GPT 기반 개발
개념
GPT-3(Generative Pre-trained Transformer 3)은 OpenAI의 최신 언어 모델이다. 하나의 문장 뒤에 오는 문장을 예측하거나, 하나의 언어에서 다른 언어를 번역할 수 있다. 
GPT-3는 매우 큰 텍스트 데이터에서 훈련된 언어 모델이다. 작동은 간단하다. 텍스트를 제공하면 모델이 비슷한 스타일과 구조에 따라 문장을 생성합니다. 이를 이용해 챗봇 같은 것을 쉽게 개발할 수 있다. 다른 다양한 데모는 여기(예제)를 참고하라.


설치 및 사용
코딩은 Python 만 필요하다. 그러나 앱을 실행하려면 다음이 필요하다.
먼저 베타 테스트를 신청해 키값을 얻는다.

github에 있는 GPT3-Sandbox를 다운로드한다. 그리고, 다음을 실행한다.
1. 가상환경 생성
python -m venv $ENV_NAME

2. 가상환경 활성화
source $ENV_NAME/bin/activate 

3. 설치
pip install -r api/requirements.txt

다음과 같은 패키지 디펜던시 에러 발생 시 requirements.txt의 해당 패키지 버전을 낮추어 재실행해본다.
패키지 의존성 에러

해당 패키지 버전 다운

제대로 설치된 패키지

4. 보안키 추가
openai.cfg 파일을 생성하고 OPENAI_KEY=$YOUR_SECRET_KEY 를 파일 내에 추가함. $YOUR_SECRET_KEY 는 'sk-somerandomcharacters' 처럼 되어 있음. 참고로, 키가 제대로 설정되어 있지 않으면, 예제 실행 시 다음과 같은 에러가 발생함.

5. 환경 변수 설정
아래와 같이 환경변수openai.cfg 위치를 설정한다.
export OPENAI_CONFIG=/path/to/config/openai.cfg 

6. Yarn 설치
yarn install

이제 설치가 끝나면 다음과 같이 예제를 테스트해본다. 
python examples/run_latex_app.py

실행 결과는 다음과 같다.

관련 예제 사용은 여기서 방법을 확인할 수 있다.

간단한 예시
Python으로 OpenAI GPT-3 API를 사용하여 웹 데모를 만들어 본다. 이 예는 영어를 계산, 수식, LaTeX로 변환하기 위한 GPT 적용 예시이다. 다음과 같이 간단히 Example(Input, Output) 형식으로 예시를 만들어 주면, 비슷한 Input example에 유사한 Output을 대답해준다.

# Construct GPT object and show some examples
gpt = GPT(engine="davinci",
          temperature=0.5,
          max_tokens=100)
gpt.add_example(Example('Two plus two equals four', '2 + 2 = 4'))
gpt.add_example(Example('The integral from zero to infinity', '\\int_0^{\\infty}'))
gpt.add_example(Example('The gradient of x squared plus two times x with respect to x', '\\nabla_x x^2 + 2x'))
gpt.add_example(Example('The log of two times x', '\\log{2x}'))
gpt.add_example(Example('x squared plus y squared plus equals z squared', 'x^2 + y^2 = z^2'))

# Define UI configuration
config = UIConfig(description="Text to equation",
                  button_text="Translate",
                  placeholder="x squared plus 2 times x")

demo_web_app(gpt, config)

이 코드를 python 스크립트로 실행하면, 새로운 입출력을 테스트할 수 있는 웹 앱이 자동으로 시작된다. 참고로, examples 디렉토리에는 이미 3 개의 예제 스크립트가 있다. 

아래는 매우 간단한 사칙연산을 학습하는 예이다. 최소한의 예시로 계산기 모델을 제공한다.
# add some calculation examples
gpt.add_example(Example("add 3+5", "8"))
gpt.add_example(Example("add 8+5", "13"))
gpt.add_example(Example("add 50+25", "75"))

다음은 자연어를 SQL 언어로 변환하는 예이다. 
# Example
gpt.add_example(Example('Fetch unique values of DEPARTMENT from Worker table.', 
                        'Select distinct DEPARTMENT from Worker;'))
gpt.add_example(Example('Print the first three characters of FIRST_NAME from Worker table.', 
                        'Select substring(FIRST_NAME,1,3) from Worker;'))

# Question
prompt = "Display the lowest salary from the Worker table."
output = gpt.submit_request(prompt)
print(output.choices[0].text)
prompt = "Tell me the count of employees working in the department HR."
print(gpt.get_top_reply(prompt))

BERT 파인튜닝
BERT 모델을 이용해 NLP관련 데이터를 본인의 목적에 맞게 파인튜닝할 수 있다. 설치를 위해 다음을 실행한다.
pip install transformers

BERT 모델은 다음과 같이 초기화할 수 있다. 
config = BertConfig()
config.num_labels = num_labels
self.bert = BertModel(config)

BertConfig는 다음 하이퍼 파라메터를 가진다. 
BertConfig {
  "attention_probs_dropout_prob": 0.1,
  "classifier_dropout": null,
  "hidden_act": "gelu",
  "hidden_dropout_prob": 0.1,
  "hidden_size": 768,
  "initializer_range": 0.02,
  "intermediate_size": 3072,
  "layer_norm_eps": 1e-12,
  "max_position_embeddings": 512,
  "model_type": "bert",
  "num_attention_heads": 12,
  "num_hidden_layers": 12,
  "pad_token_id": 0,
  "position_embedding_type": "absolute",
  "transformers_version": "4.12.5",
  "type_vocab_size": 2,
  "use_cache": true,
  "vocab_size": 30522
}

우리는 사전정의된 BERT 기반 모델을 사용한다. 
from transformers import BertModel
model = BertModel.from_pretrained("bert-base-cased")

이 결과, 사전 학습 모델이 로딩된다. 가중치가 설정되었으므로, 파인튜닝(미세조정)이 가능한다. 

텍스트 입력은 각 단어가 토큰화되어 임베딩 처리되어야 한다. 
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')

예를 들어 문장이 아래와 같다면,
sentence = ‘I love kids’

토큰처리 한 결과는 다음과 같다.
tokens = tokenizer.tokenize(sentence)
print(tokens)
# [‘i’, ‘love’, ‘kids’]
tokens = [‘[CLS]’] + tokens + [‘[SEP]’]
# [‘[CLS]’, ‘[CLS]’, ‘i’, ‘love’, ‘kids’, ‘[SEP]’, ‘[SEP]’]
print(tokens)
tokens = tokens + [‘[PAD]’] + [‘[PAD]’]

처리되지 않아야하는 패딩 토큰은 attention_mask로 계산되지 않도록 한다.
attention_mask = [1 if i!= ‘[PAD]’ else 0 for i in tokens]
print(attention_mask)
# [1, 1, 1, 1, 1, 1, 1, 0, 0]

이를 임베딩으로 변환한 결과는 다음과 같다. 
token_ids = tokenizer.convert_tokens_to_ids(tokens)
# [101, 101, 1045, 2293, 4268, 102, 102, 0, 0]

이 값을 텐서로 변환한다.
token_ids = torch.tensor(token_ids).unsqueeze(0)
attention_mask = torch.tensor(attention_mask).unsqueeze(0)

Bert Model을 통해, 다양한 목적으로 파인튜닝 가능하다. 다음은 임상 데이터를 이용해, 환자에게 진료를 제안하는 모델을 학습하는 예시이다. 

import pandas as pd

X = """"
How do I use this inhaler?", Medication Inquiry
"What should I bring to my first visit?", General Inquiry
"Sore throat and runny nose", Symptom Check
"I have a fever and a cough", Symptom Check
"Do you offer COVID-19 vaccinations?", General Inquiry
"Do I need a prescription for this medicine?", Medication Inquiry
"I'd like to schedule a check-up" ,Appointment Booking
"I have a fever and a cough" ,Symptom Check
"Can you tell me about the side effects of this medication?", Medication Inquiry
"I'd like to schedule a check-up", Appointment Booking
"What are your opening hours?", General Inquiry
"Is this medication covered by insurance?", Medication Inquiry
"Can you tell me about the side effects of this medication?", Medication Inquiry
"Headache and dizziness", Symptom Check
"What are your opening hours?", General Inquiry
"Chest pain or discomfort", Symptom Check
"What are the alternatives to this drug?", Medication Inquiry
"Looking to book a follow-up appointment" ,Appointment Booking
"How do I use this inhaler?", Medication Inquiry
"Stomach pain and nausea", Symptom Check
"Is this medication covered by insurance?", Medication Inquiry
"Do I need a prescription for this medicine?", Medication Inquiry
"Are telehealth consultations available?", General Inquiry
"I'd like to schedule a check-up", Appointment Booking
"Stomach pain and nausea", Symptom Check
"Is this medication covered by insurance?", Medication Inquiry
"I want to see a dermatologist", Appointment Booking
"How do I use this inhaler?", Medication Inquiry
"How do I use this inhaler?", Medication Inquiry
"Can I get a prescription refill?", General Inquiry
"Looking to book a follow-up appointment", Appointment Booking
"What are the alternatives to this drug?", Medication Inquiry
"I have a fever and a cough", Symptom Check
"Do I need a prescription for this medicine?", Medication Inquiry
"Can you tell me about the side effects of this medication?", Medication Inquiry
"I want to see a dermatologist", Appointment Booking
"Muscle aches and joint pain", Symptom Check
"How can I cancel my appointment?", General Inquiry
"Need to book an appointment for vaccination", Appointment Booking
"I want to see a dermatologist", Appointment Booking
"Do I need a prescription for this medicine?", Medication Inquiry
"Can you tell me about the side effects of this medication?", Medication Inquiry
"What are the alternatives to this drug?",Medication Inquiry
"Shortness of breath for two days", Symptom Check
"Persistent coughing", Symptom Check
"Fever with chills", Symptom Check
"Muscle aches and joint pain" ,Symptom Check
"Looking to book a follow-up appointment" ,Appointment Booking
"How do I use this inhaler?", Medication Inquiry
"Can you tell me about the side effects of this medication?", Medication Inquiry
"What are the alternatives to this drug?", Medication Inquiry
"I'd like to schedule a check-up" ,Appointment Booking
"What are the alternatives to this drug?", Medication Inquiry
"What should I bring to my first visit?", General Inquiry
"How do I use this inhaler?", Medication Inquiry
"Can you tell me about the side effects of this medication?", Medication Inquiry
"I'd like to schedule a check-up", Appointment Booking
"Shortness of breath for two days", Symptom Check
"Do I need a prescription for this medicine?", Medication Inquiry
"Are telehealth consultations available?", General Inquiry
"I want to see a dermatologist", Appointment Booking
"I have a fever and a cough", Symptom Check
"I want to see a dermatologist", Appointment Booking
"I need an appointment for a sore throat", Appointment Booking
"Sore throat and runny nose", Symptom Check
"Sore throat and runny nose", Symptom Check
"Persistent coughing", Symptom Check
"What are the alternatives to this drug?", Medication Inquiry
"How do I use this inhaler?", Medication Inquiry
"I have a fever and a cough", Symptom Check
"What are the alternatives to this drug?", Medication Inquiry
"Looking to book a follow-up appointment", Appointment Booking
"How can I cancel my appointment?", General Inquiry
"Are telehealth consultations available?", General Inquiry
"Can I get a prescription refill?", General Inquiry
"What should I bring to my first visit?", General Inquiry
"Looking to book a follow-up appointment", Appointment Booking
"What insurances do you accept?", General Inquiry
"How do I book an appointment?", General Inquiry
"Can you tell me about the side effects of this medication?", Medication Inquiry
"Headache and dizziness" ,Symptom Check
"How do I use this inhaler?" Medication Inquiry
"Can you tell me about the side effects of this medication?", Medication Inquiry
"Can you tell me about the side effects of this medication?" ,Medication Inquiry
"What are the alternatives to this drug?", Medication Inquiry
"What insurances do you accept?", General Inquiry
"Can I get a prescription refill?" ,General Inquiry
"Can you tell me about the side effects of this medication?" ,Medication Inquiry
"Unexplained weight loss", Symptom Check
"I have a fever and a cough" ,Symptom Check
"What insurances do you accept?", General Inquiry
"Looking to book a follow-up appointment", Appointment Booking
"Can you tell me about the side effects of this medication?",Medication Inquiry
"Is this medication covered by insurance?", Medication Inquiry
"Fever with chills", Symptom Check
"Headache and dizziness", Symptom Check
"Do I need a prescription for this medicine?", Medication Inquiry
"Chest pain or discomfort",Symptom Check
"Need to book an appointment for vaccination", Appointment Booking
"Need to book an appointment for vaccination", Appointment Booking
"Do I need a prescription for this medicine?", Medication Inquiry
"Looking to book a follow-up appointment" ,Appointment Booking
"What are your opening hours?", General Inquiry
"""

lines = X.strip().split("\n")

# 앞의 학습 데이터를 질문과 의도로 분리한다.
query = []
intent = []
for line in lines:
    parts = line.split(',')  # Split by comma
    if len(parts) >= 2:  # Ensure there are at least two parts
        # Strip leading/trailing spaces from each part and handle potential quotation marks
        query_part = parts[0].strip().strip('"')
        intent_part = parts[1].strip()

        query.append(query_part)
        intent.append(intent_part)
    else:
        print(f"Could not split line: {line}")

# 데이터 프레임 생성
df = pd.DataFrame({
    'query': query,
    'intent': intent
})

print(df)

# BERT 임포트
from transformers import BertTokenizer
from sklearn.model_selection import train_test_split
from torch.utils.data import TensorDataset, DataLoader, RandomSampler, SequentialSampler
import torch

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

# 각 질문을 토큰처리함.
input_ids = []
attention_masks = []

for query in df['query']:
    encoded_dict = tokenizer.encode_plus(
        query,
        add_special_tokens=True,  # Add '[CLS]' and '[SEP]'
        max_length=64,  # 최대 문자열 크기 설정
        pad_to_max_length=True,
        return_attention_mask=True,  # Construct attention masks
        return_tensors='pt',  # Return pytorch tensors
    )
    input_ids.append(encoded_dict['input_ids'])
    attention_masks.append(encoded_dict['attention_mask'])

# 텐서로 변환
input_ids = torch.cat(input_ids, dim=0)
attention_masks = torch.cat(attention_masks, dim=0)
labels = torch.tensor(df['intent'].factorize()[0])  # Convert textual labels to integers

# 데이터셋을 학습, 검증용으로 분리
train_inputs, validation_inputs, train_labels, validation_labels = train_test_split(input_ids, labels, random_state=2018, test_size=0.1)
train_masks, validation_masks, _, _ = train_test_split(attention_masks, labels, random_state=2018, test_size=0.1)

# 학습 데이터셋 생성
train_data = TensorDataset(train_inputs, train_masks, train_labels)
train_sampler = RandomSampler(train_data)
train_dataloader = DataLoader(train_data, sampler=train_sampler, batch_size=32)

# 테스트 검증 데이터셋 생성
validation_data = TensorDataset(validation_inputs, validation_masks, validation_labels)
validation_sampler = SequentialSampler(validation_data)
validation_dataloader = DataLoader(validation_data, sampler=validation_sampler, batch_size=32)

from transformers import BertForSequenceClassification, AdamW, BertConfig
from transformers import get_linear_schedule_with_warmup
import random
import numpy as np

epochs = 20

# Load BertForSequenceClassification, the pretrained BERT model with a single linear classification layer on top
model = BertForSequenceClassification.from_pretrained(
    "bert-base-uncased",  # Use the 12-layer BERT model, with an uncased vocab
    num_labels = len(df['intent'].unique()),  # 라벨 수
    output_attentions = False,  # 어텐션 가중치 리턴 여부
    output_hidden_states = False,  # 은닉상태 리턴 여부
)

model.cuda()

# 학습 최적화 솔류션 설정
optimizer = AdamW(model.parameters(),
                  lr = 2e-5,  # args.learning_rate
                  eps = 1e-8  # args.adam_epsilon
                )

# Total number of training steps is [number of batches] x [number of epochs]
total_steps = len(train_dataloader) * epochs

# Create the learning rate scheduler
scheduler = get_linear_schedule_with_warmup(optimizer,
                                            num_warmup_steps = 0,
                                            num_training_steps = total_steps)

import torch
epochs = 100

# Check if CUDA (GPU support) is available
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

for epoch in range(epochs):
    print(f"Epoch {epoch + 1}/{epochs}")
    print('-' * 10)

    # Set the model to training mode
    model.train()

    # Tracking variables
    total_train_loss = 0

    # Iterate over each batch of training data...
    for step, batch in enumerate(train_dataloader):
        # Progress update every 40 batches.
        if step % 40 == 0 and not step == 0:
            print(f"  Batch {step}  of  {len(train_dataloader)}.")

        # Unpack this training batch from our dataloader and copy each tensor to the GPU
        b_input_ids = batch[0].to(device)
        b_attention_mask = batch[1].to(device)
        b_labels = batch[2].to(device)

        # Always clear any previously calculated gradients before performing a backward pass
        model.zero_grad()

        # Perform a forward pass (evaluate the model on this training batch)
        outputs = model(b_input_ids,
                        token_type_ids=None,
                        attention_mask=b_attention_mask,
                        labels=b_labels)
        loss = outputs.loss

        # Accumulate the training loss over all of the batches so that we can
        # calculate the average loss at the end
        total_train_loss += loss.item()

        # Perform a backward pass to calculate the gradients
        loss.backward()

        # Clip the norm of the gradients to 1.0 to prevent "exploding gradients"
        torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)

        # Update parameters and take a step using the computed gradient
        optimizer.step()

        # Update the learning rate
        scheduler.step()

    # Calculate the average loss over the training data
    avg_train_loss = total_train_loss / len(train_dataloader)

    print(f"  Average training loss: {avg_train_loss:.2f}")


import torch
from transformers import BertForSequenceClassification

# Initialize and train the model
model = BertForSequenceClassification.from_pretrained("bert-base-uncased")
# Train the model...

# 학습 모델 저장
model_weights_path = "/content/model/model_weights.pth"
torch.save(model.state_dict(), model_weights_path)

from transformers import BertConfig

# Save the model's configuration
model_config_path = "/content/model/model_config.json"
model.config.save_pretrained(model_config_path)

# 파인튜닝된 학습 모델 테스트
import torch
from transformers import BertForSequenceClassification, BertTokenizer

# Load the model configuration and weights
model_config_path = "/content/model/model_config.json"
model_weights_path = "/content/model/model_weights.pth"

# Load the model configuration
model_config = BertConfig.from_pretrained(model_config_path)

model = BertForSequenceClassification(config=model_config)
model.load_state_dict(torch.load(model_weights_path))
model.eval()

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

user_input = "Book appointment for me "
tokens = tokenizer.encode_plus(user_input, max_length=128, truncation=True, padding="max_length", return_tensors="pt")
with torch.no_grad():
    outputs = model(**tokens)
    logits = outputs.logits

predicted_label = torch.argmax(logits, dim=1).item()

intents = ["Medication Inquiry", "General Inquiry", "Symptom Check", "Appointment Booking"]
predicted_intent = intents[predicted_label]

print("Predicted Intent:", predicted_intent)

기타, 다음은 QA 데이터를 학습하는 과정을 보여준다. 
from transformers import AutoTokenizer, AutoModelForSequenceClassification, AutoModelForQuestionAnswering

tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")

model = AutoModelForSequenceClassification.from_pretrained("bert-base-uncased", num_labels=2)

text = [
    "Jim Henson was a nice puppet.",
    "In fact Jim Henson was a nice puppet.",
    "I tell you Jim Henson really was just a nice puppet."
]

inputs = tokenizer(text, return_tensors="pt", padding=True, truncation=True)

output = model(**inputs)
print(output)

model = AutoModelForQuestionAnswering.from_pretrained("bert-base-uncased")

question = [
    "Who was Jim Henson?",
    "Who really was Jim Henson?",
    "Tell me who really was Jim Henson?"
]

text = [
    "Jim Henson was a nice puppet.",
    "In fact Jim Henson was a nice puppet.",
    "I tell you Jim Henson really was just a nice puppet."
]

inputs = tokenizer(question, text, return_tensors="pt", padding=True, truncation=True)

print(inputs)
print(tokenizer.batch_decode(inputs['input_ids']))

output = model(**inputs)
print(output)


BERT 동작 메커니즘 분석
토크나이저는 두 가지 주요 기능을 제공한다.
  • 원시 텍스트를 단어, 하위 단어 또는 문자 수준에서 토큰이라고 하는 더 작은 부분으로 분할한다.
  • 토큰을 기계 학습 모델이 이해할 수 있는 숫자 형식으로 변환한다.
텍스트를 하위 단어 단위로 분할하면 어휘 크기와 시퀀스 길이 사이의 균형이 잘 잡힌다. 또한 희귀하고 어휘가 없는 단어를 더 잘 처리하여 알 수 없는 토큰으로 처리할 필요성을 줄인다.
WordPiece는 BERT 토크나이저에서 사용되는 특정 하위 단어 토큰화 알고리즘으로, 텍스트 데이터 세트에서 점수가 가장 높은 문자 쌍을 선택하고 병합하는 프로세스를 통해 토큰의 어휘를 정의된 크기로 구축하도록 설계되었다. 이 점수는 개별 요소의 빈도 곱에 대한 쌍의 빈도의 비율이다.

score = (freq_of_pair) / (freq_of_first_element × freq_of_second_element)

여기서는 Hugging Face BERT Tokenizer의 인스턴스를 사용할 것이다. BERT 토크나이저에 전달된 텍스트는 일련의 토큰으로 나눈다. 어휘의 각 고유 토큰에는 정수 ID가 할당된다. 이것은 원-핫 인코딩된 벡터의 축약형 또는 추상화 역할을 한다.

원-핫 인코딩된 벡터는 표시되는 토큰에 해당하는 인덱스의 요소가 1의 값을 가지며 다른 모든 요소의 값이 0인 벡터일 뿐이다. 벡터의 길이는 텍스트 데이터셋의 어휘에 있는 토큰의 개수와 같다.

원-핫 인코딩된 벡터와 정수 ID는 모두 조회 테이블에서 해당 행을 선택하는 동일한 용도로 사용된다.

처리된 텍스트에는 세 가지 특수 토큰이 있다.
  • 분류 토큰: [CLS] 토큰은 시퀀스의 맨 처음에 배치됩니다. 전체 시퀀스에 대한 집계 표현 역할을 할 수 있다.
  • 분리 토큰: [SEP] 토큰은 시퀀스의 모든 문장 끝에 배치됩니다. 이를 통해 BERT는 문장을 구별하고 문장 경계를 이해할 수 있다.
  • 패딩 토큰: [PAD] 토큰은 시퀀스의 끝에 배치되어 일괄 처리의 모든 시퀀스 길이가 동일하도록 시퀀스 길이를 확장합니다. 이는 여러 샘플(배치)을 병렬로 처리할 수 있도록 한다. 이는 선형 대수학 계산 요구사항이다.
처리된 텍스트를 기반으로 하는 세 가지 유형의 정수 인코딩은 다음과 같다.
  • 입력 ID: BERT 기본 모델에는 30522개의 토큰으로 정의된 어휘가 있다. 각 고유 문자열 토큰은 고유한 정수 ID에 매핑됩니다.  
  • 주의 마스크: 정의된 최대 시퀀스 길이보다 짧은 시퀀스는 패딩 토큰으로 확장된다. 패딩 토큰은 자리 표시자일 뿐이며 모델의 출력에 영향을 주어서는 안 된다. 어텐션 마스크는 패딩 토큰을 나머지 모든 토큰과 구별된다. 패딩이 아닌 토큰의 인덱스에 있는 요소는 1의 값을 받고 패딩 토큰의 인덱스에 있는 요소는 0을 받다.
  • 토큰 유형 ID: 다음 문장 예측은 BERT 모델을 사전 학습할 때 수행되는 특수 작업이다. 시퀀스 입력은 두 문장으로 구성되도록 구성된다. 두 번째 문장은 원본 텍스트의 첫 번째 문장 바로 다음에 나오는 문장이거나 원본 텍스트의 임의의 문장이다. 목표는 두 번째 문장이 실제로 다음 문장인지 아니면 임의의 문장인지 예측하는 것이다. 토큰 유형 ID는 시퀀스에서 첫 번째 문장에 속하는 토큰과 두 번째 문장에 속하는 토큰을 각각 0과 1을 할당하여 지정한다. 다음 문장 예측을 수행하는 것이 아니라 시퀀스 분류라는 작업을 수행하기 때문에 시퀀스의 모든 토큰은 동일하게 처리되고 0으로 인코딩된다. 토큰 유형 ID는 시퀀스 분류를 수행할 때 모델 출력에 영향을 주지 않다.
임베딩은 토큰의 벡터 표현으로, 이를 통해 본질적으로 공간에서 토큰을 구성할 수 있다. 이상적으로 비슷한 의미의 토큰은 서로 더 가깝게 배치되고 다른 의미의 토큰은 더 멀리 떨어져 있다. 공간이 클수록 토큰을 정리할 수 있는 공간이 더 많아집니다.

임베딩 길이는 큰 어휘와 문맥 의미의 복잡성을 수용하기 위해 상대적으로 높게 설정된다. BERT 베이스의 임베딩 길이는 768이다.

각 토큰에 대해 다음 세 가지 임베딩이 생성되고 함께 추가된다.
  • 단어 임베딩: 각 입력 ID는 단어 임베딩 조회 테이블의 해당 행 벡터로 바뀝니다. 이 조회 테이블에는 어휘의 각 고유 토큰에 대해 하나의 행이 있다. BERT base의 어휘 크기는 30522이다.
  • 토큰 유형 임베딩: 토큰이 있는 문장(첫 번째 또는 두 번째)에 해당하는 이 조회 테이블에 두 개의 행만 있다는 점을 제외하고는 임베딩이라는 단어와 동일한 원칙이다. 이것은 다음 문장 예측의 사전 학습 작업에만 관련이 있음을 기억하십시오. 그렇지 않으면 모델 출력에 영향을 주지 않다.
  • 위치 임베딩: 위치 임베딩은 시퀀스에서 토큰의 순서에 대한 감각을 모델에 제공하는 역할을 한다. 조회 테이블에서 해당 위치 임베딩을 검색하는 데 사용되는 위치 ID는 각 시퀀스에 대해 동일한다 — 0에서 n까지의 연속 정수로 구성된 벡터, 여기서 n은 최대 시퀀스 길이이다. BERT 베이스의 최대 시퀀스 길이는 512이다.
이러한 세 가지 임베딩을 모두 추가하면, 정보의 다양한 측면이 단일 임베딩으로 인코딩된다. 이제 임베딩에는 시퀀스에 있는 토큰의 의미론적 의미, 시퀀스에서 토큰의 요소별 위치 및 토큰이 있는 문장에 대한 정보가 포함되었다. 

이러한 모든 임베딩은 대규모 텍스트 말뭉치에 대한 사전 학습 단계에서 학습되며 다운스트림 작업을 위해 추가로 미세 조정할 수 있다. 다음 코드는 임베딩을 구현한 것이다. 
class BertEmbeddings(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.config = config
        self.word_embeddings = nn.Embedding(config.vocab_size, config.emb_size, padding_idx=config.pad_token_id)
        self.position_embeddings = nn.Embedding(config.max_seq_length, config.emb_size)
        self.token_type_embeddings = nn.Embedding(2, config.emb_size)
        self.LayerNorm = nn.LayerNorm(config.emb_size, eps=config.layer_norm_eps)
        self.dropout = nn.Dropout(config.dropout)
        
        # position ids (used in the pos_emb lookup table) that we do not want updated through backpropogation
        self.register_buffer("position_ids", torch.arange(config.max_seq_length).expand((1, -1)))

    def forward(self, input_ids, token_type_ids):
        word_emb = self.word_embeddings(input_ids)
        pos_emb = self.position_embeddings(self.position_ids)
        type_emb = self.token_type_embeddings(token_type_ids)

        emb = word_emb + pos_emb + type_emb
        emb = self.LayerNorm(emb)
        emb = self.dropout(emb)

        return emb

소스에서 config.pad_token_id는 단어 임베딩 조회 테이블에 제공되어 해당 행을 모두 0으로 설정하고 고정한다. 패딩 임베딩은 자리 표시자일 뿐이며 모델 출력에 영향을 미치지 않아야 한다. 

position_embeddings는 각 시퀀스에 대해 동일하다. 최대 시퀀스 길이가 512인 경우 0-511을 가진다. 이후, 레이어 정규화와 드롭아웃이 적용된다. 이렇게 하면 소실/폭발 그래디언트 문제를 처리하는 데 도움이 되며 안정적인 학습으로 이어진다.

드롭아웃으로 임베딩의 각 요소를 어느 정도의 확률(BERT의 경우 p=0.1)로 제로화하여 과적합을 방지한다. 결과적으로 모델은 새 데이터에 대해 더 잘 일반화된다. 

BERT기본 모델에는 12개 트랜스포머 레이어가 있는 인코더가 포함된다. 레이어는 서로 동일하며 한 레이어의 출력은 순차적으로 다음 레이어에 공급된다.

각 레이어에는 다중 헤드 셀프 어텐션(multi-headed self attention)과 포인트별 피드포워드 네트워크(point-wise feed forward network)가 있다. 인코더에 레이어가 많을수록 일반적으로 모델이 텍스트에서 더 복잡한 패턴과 관계를 학습할 수 있다. 이를 통해, 하위 계층 지역, 구문 관계, 중간 계층의 문장 수준, 의미 체계, 상위 계층의 문서 수준 관계를 학습한다.

멀티헤드 어텐션 
셀프-어텐션 메커니즘은 모델이 시퀀스에 있는 다른 모든 단어의 맥락에서 각 단어의 중요성을 평가할 수 있도록 한다. 이 프로세스를 통해 각 토큰의 임베딩 벡터는 주변 토큰과 어떻게 관련되는지에 대한 정보로 보강된다.

셀프 어텐션이 수행되기 전에 동일한 ID를 가진 모든 토큰은 시퀀스의 다른 토큰에 관계없이 동일한 임베딩을 갖다. 셀프 어텐션은 시퀀스의 각 토큰을 다른 모든 토큰과 비교한 다음 관련 관계 및 컨텍스트 정보로 각 토큰 임베딩 벡터를 보강한다.

각 토큰의 임베딩 벡터는 해당 차원을 유지하는 선형 계층을 통해 실행된다. 각각의 가중치(W)를 사용한 행렬 곱셈을 통해 토큰 임베딩을 Query, Key 및 Value 벡터에 프로젝션한다. 

내적은 두 벡터 간의 유사성 측도로 사용된다. 내적은 스칼라를 생성하며, 스케일러 값이 클수록 두 벡터가 더 유사한다.

행렬 곱셈을 사용하면 각 토큰의 내적을 다른 모든 토큰과 함께 한 번에 취할 수 있다. 키 행렬(K)은 행렬 곱셈 규칙으로 인해 전치(K^T)되어야 한다. 결과로 생성되는 QK의 각 행 벡터. T 행렬은 쿼리 행렬(Q)의 동일한 행 벡터(토큰)에 대응된다. 그리고 QK의 행 벡터에 있는 각 요소. T 행렬은 Q의 해당 토큰이 K의 동일한 시퀀스의 각 토큰과 얼마나 유사한지 알려준다.

쿼리(Q) 행렬에 키(K.T)를 곱하면 점수 유사도 행렬(QK)이 생성된다. 

유사도 점수는 K에 있는 벡터의 차원 크기의 제곱근으로 나뉜다. 패딩 토큰과 관련된 유사성 점수는 음의 무한대로 설정된다. softmax 함수는 행 차원을 따라 유사성 점수에 적용되어 1을 더하는 가중치의 확률 분포를 구한다.
softmax 함수를 적용하기 전에 유사성 점수를 축소하면 분포가 평활화되고, 역전파 중에 기울기가 사라지는 것을 방지할 수 있다.

스케일링된 유사성 점수 행렬에 값(V)을 곱하여 임베딩이 강화된다. 

이 평균값이 시퀀스의 특정 토큰이 다른 토큰에 주의를 기울이는 정도가 된다. 이를 가중치를 수정해 솔류션과 가깝게 되도록 학습한다.

이제 셀프 어텐션을 위한 코드가 어떻게 구현되는지 살펴보겠다.
class BertSelfAttention(nn.Module):
    def __init__(self, config, layer_i):
        super().__init__()
        self.config = config
        self.n_heads = config.n_heads[layer_i]
        self.head_size = config.emb_size // self.n_heads
        self.query = nn.Linear(config.emb_size, config.emb_size)
        self.key = nn.Linear(config.emb_size, config.emb_size)
        self.value = nn.Linear(config.emb_size, config.emb_size)
        self.dropout = nn.Dropout(config.dropout)


    def forward(self, emb, att_mask):
        B, T, C = emb.shape  # batch size, sequence length, embedding size   
    
        q = self.query(emb).view(B, T, self.n_heads, self.head_size).transpose(1, 2)
        k = self.key(emb).view(B, T, self.n_heads, self.head_size).transpose(1, 2)
        v = self.value(emb).view(B, T, self.n_heads, self.head_size).transpose(1, 2)
        
        weights = q @ k.transpose(-2, -1) * self.head_size**-0.5

        # set the pad tokens to -inf so that they equal zero after softmax
        if att_mask != None:
            att_mask = (att_mask > 0).unsqueeze(1).repeat(1, att_mask.size(1), 1).unsqueeze(1)
            weights = weights.masked_fill(att_mask == 0, float('-inf'))

        weights = F.softmax(weights, dim=-1)
        weights = self.dropout(weights)
        
        emb_rich = weights @ v
        emb_rich = emb_rich.transpose(1, 2).contiguous().view(B, T, C)
    
        return emb_rich

class BertSelfOutput(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.dense = nn.Linear(config.emb_size, config.emb_size)
        self.dropout = nn.Dropout(config.dropout)
        self.LayerNorm = nn.LayerNorm(config.emb_size, eps=config.layer_norm_eps)
        
    def forward(self, emb_rich, emb):
        x = self.dense(emb_rich)
        x = self.dropout(x)
        x = x + emb
        out = self.LayerNorm(x)

        return out

class BertAttention(nn.Module):
    def __init__(self, config, layer_i):
        super().__init__()
        self.self = BertSelfAttention(config, layer_i)
        self.output = BertSelfOutput(config)

    def forward(self, emb, att_mask):
        emb_rich = self.self(emb, att_mask)
        out = self.output(emb_rich, emb)

        return out

셀프-어텐션 메커니즘은 세 가지 클래스로 구성된다.

BertSelfAttention: self-attention 구현을 담당한다
BertSelfOutput: BertSelfAttention 출력에 다양한 작업을 적용한다.
BertAttention: 위의 두 모듈을 보관하여 순서대로 실행할 수 있다.

BERT는 다중 헤드 어텐션(multi-headed attention)이라는 것을 사용한다. 이는 서로 독립적으로 작동하는 여러 개의 자기 주의 메커니즘이다. 각 트랜스포머 레이어는 12개가 있다. 각 헤드의 크기는 원래 임베딩 벡터 크기를 헤드 수로 균등하게 나눈 값이다. self-attention의 출력은 BertSelfOutput 클래스에서 추가로 처리된다. 

위치별 피드포워드 네트워크는 트랜스포머 계층의 두 번째 주요 구성 요소이며, 학습 가능한 추가 매개변수를 사용하여 모델 성능을 늘리는 역할을 한다. 

ReLU와 같은 다른 활성화 함수에 비해 GeLU의 두 가지 장점은 다음과 같다. GeLU는 매끄럽고(모든 곳에서 미분 가능) 최적화와 수렴에 도움이 된다. 음수 입력의 경우 음수 기울기를 사용하여 역전파 중에 더 나은 정보 흐름을 허용하여 학습에 도움이 된다.

전체 트랜스포머 레이어는 BertLayer에 함께 모여 BertAttention, BertIntermediate 및 BertOutput이 순서대로 처리된다.
class BertIntermediate(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.dense = nn.Linear(config.emb_size, config.intermediate_size)
        self.gelu = nn.GELU() 

    def forward(self, att_out):
        x = self.dense(att_out)
        out = self.gelu(x)

        return out


class BertOutput(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.dense = nn.Linear(config.intermediate_size, config.emb_size)
        self.dropout = nn.Dropout(config.dropout)
        self.LayerNorm = nn.LayerNorm(config.emb_size, eps=config.layer_norm_eps) 

    def forward(self, intermediate_out, att_out):
        x = self.dense(intermediate_out)
        x = self.dropout(x)
        x = x + att_out
        out = self.LayerNorm(x)

        return out 

BertForSequenceClassification 클래스는 BERT 모델을 인스턴스화하기 위해 호출하는 가장 바깥쪽 클래스이다. 여기에는 기본 아키텍처(self.bert)와 분류 헤드(self.classifier)가 모두 있다.
class BertLayer(nn.Module):
    def __init__(self, config, layer_i):
        super().__init__()
        self.attention = BertAttention(config, layer_i)
        self.intermediate = BertIntermediate(config)
        self.output = BertOutput(config) 

    def forward(self, emb, att_mask):
        att_out = self.attention(emb, att_mask)
        intermediate_out = self.intermediate(att_out)
        out = self.output(intermediate_out, att_out)

        return out

class BertPooler(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.dense = nn.Linear(config.emb_size, config.emb_size)
        self.tanh = nn.Tanh()

    def forward(self, encoder_out):
        pool_first_token = encoder_out[:, 0]
        out = self.dense(pool_first_token)
        out = self.tanh(out)
        return out

class BertModel(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.embeddings = BertEmbeddings(config)
        self.encoder = BertEncoder(config)
        self.pooler = BertPooler(config)

    def forward(self, input_ids, token_type_ids, att_mask):
        emb = self.embeddings(input_ids, token_type_ids)
        out = self.encoder(emb, att_mask)
        pooled_out = self.pooler(out)
        return out, pooled_out

class BertForSequenceClassification(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.config = config
        self.bert = BertModel(config)
        self.dropout = nn.Dropout(config.dropout)
        self.classifier = nn.Linear(config.emb_size, config.n_classes)

    def forward(self, input_ids, token_type_ids, attention_mask=None):
        _, pooled_out = self.bert(input_ids, token_type_ids, attention_mask)
        pooled_out = self.dropout(pooled_out)
        logits = self.classifier(pooled_out)
        
        if self.config.return_pooler_output:
            return pooled_out, logits
        
        return logits

출력은 각 클래스에 대해 하나의 값이 있는 로짓이다. 이러한 로짓의 최대값을 취하면 예측된 클래스를 얻을 수 있다. 그러나 로짓을 확률로 해석하려면 softmax 함수를 적용해야 한다.

마무리
BERT이외에도, 허깅페이스의 transformers 는 다음과 같은 사전학습 모델을 로딩해, 파인튜닝할 수 있다. 
  • RoBERTa: Robustly optimized BERT approach
  • XLNet: Bidirecftional context support
  • GPT: Generative Pretrained Transformer
  • T5: Text to text transfer transformer
  • ELECTRA: Effeciently learning an encoder that classifiers token replacements accurately
  • ALBERT: A Lite BERT
  • DistilBERT: smaller version of BERT using distillation techniques

레퍼런스