BERT 완벽 가이드: 자연어처리의 혁명적 모델

BERT(Bidirectional Encoder Representations from Transformers)는 2018년 구글이 발표한 혁신적인 자연어처리 모델입니다. 양방향 문맥 이해를 통해 NLP 분야에 새로운 패러다임을 제시한 BERT의 아키텍처, 학습 방법, 실전 활용법까지 상세히 알아봅니다.

BERT 완벽 가이드: 자연어처리의 혁명적 모델

BERT 완벽 가이드: 자연어처리의 혁명적 모델

들어가며

2018년 10월, 구글 AI 팀이 발표한 BERT(Bidirectional Encoder Representations from Transformers)는 자연어처리(NLP) 분야에 혁명적인 변화를 가져왔습니다. BERT는 발표 직후 11개의 NLP 벤치마크에서 최고 성능을 달성하며, 기존 모델들을 압도적으로 뛰어넘는 결과를 보여주었습니다.

BERT가 특별한 이유는 양방향(Bidirectional) 문맥 이해에 있습니다. 기존의 언어 모델들이 텍스트를 왼쪽에서 오른쪽으로(또는 그 반대로) 한 방향으로만 처리했던 것과 달리, BERT는 문장의 모든 단어가 양쪽 문맥을 동시에 참조할 수 있습니다. 이를 통해 인간이 언어를 이해하는 방식에 더 가깝게 문맥을 파악할 수 있게 되었습니다.

이 글에서는 BERT의 핵심 개념부터 실제 구현까지, NLP 실무자와 연구자를 위한 종합적인 가이드를 제공합니다.


1. BERT 이전의 NLP: 왜 BERT가 필요했는가?

1.1 전통적인 언어 모델의 한계

BERT 이전의 언어 모델들은 크게 두 가지 접근법을 사용했습니다:

단방향 언어 모델 (Unidirectional LM)

  • GPT 계열: 왼쪽에서 오른쪽으로만 문맥 참조
  • ELMo: 양방향이지만 독립적으로 학습 후 연결
예시: "The bank of the river was steep."
- 단방향: "bank"를 이해할 때 "The"만 참조
- BERT: "bank"를 이해할 때 "river"도 함께 참조 → "강둑"으로 올바르게 해석

1.2 ELMo의 접근법과 한계

ELMo(Embeddings from Language Models)는 양방향 LSTM을 사용했지만, 순방향과 역방향을 독립적으로 학습한 후 연결(concatenate)하는 방식이었습니다. 이는 진정한 의미의 양방향 문맥 이해가 아니었습니다.

# ELMo 스타일 (개념적 표현)
forward_hidden = LSTM_forward(sentence)   # → 방향
backward_hidden = LSTM_backward(sentence) # ← 방향
combined = concatenate(forward_hidden, backward_hidden)  # 단순 연결

1.3 BERT의 혁신: 진정한 양방향성

BERT는 Transformer의 Self-Attention 메커니즘을 활용하여, 모든 단어가 문장 내 모든 다른 단어와 직접적으로 상호작용할 수 있게 했습니다.


2. BERT 아키텍처 심층 분석

2.1 전체 구조

BERT는 Transformer의 인코더(Encoder) 부분만을 사용합니다. 주요 구성요소는 다음과 같습니다:

구성요소 BERT-Base BERT-Large
Transformer 레이어 수 (L) 12 24
Hidden Size (H) 768 1024
Attention Heads (A) 12 16
총 파라미터 수 110M 340M

2.2 입력 표현 (Input Representation)

BERT의 입력은 세 가지 임베딩의 합으로 구성됩니다:

Input = Token Embedding + Segment Embedding + Position Embedding

Token Embedding: WordPiece 토큰화를 사용하여 30,000개의 어휘로 텍스트를 분할합니다.

from transformers import BertTokenizer

tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
tokens = tokenizer.tokenize("I love natural language processing")
print(tokens)
# ['i', 'love', 'natural', 'language', 'processing']

# 희귀 단어의 경우 서브워드로 분할
tokens = tokenizer.tokenize("unbelievable")
print(tokens)
# ['un', '##believe', '##able']

Segment Embedding: 문장 쌍을 구분하기 위한 임베딩 (문장 A: 0, 문장 B: 1)

Position Embedding: 토큰의 위치 정보를 인코딩 (최대 512 토큰)

2.3 특수 토큰

BERT는 세 가지 특수 토큰을 사용합니다:

  • [CLS]: 문장의 시작을 나타내며, 분류 태스크에서 전체 시퀀스의 표현으로 사용
  • [SEP]: 문장의 끝 또는 두 문장 사이의 구분자
  • [MASK]: MLM 학습 시 마스킹된 토큰
입력 예시:
[CLS] 첫 번째 문장입니다 [SEP] 두 번째 문장입니다 [SEP]

2.4 Self-Attention 메커니즘

BERT의 핵심인 Multi-Head Self-Attention은 다음과 같이 작동합니다:

Attention(Q,K,V)=softmax(QKTdk)V\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V
import torch
import torch.nn as nn
import math

class SelfAttention(nn.Module):
    def __init__(self, hidden_size, num_heads):
        super().__init__()
        self.num_heads = num_heads
        self.head_dim = hidden_size // num_heads

        self.query = nn.Linear(hidden_size, hidden_size)
        self.key = nn.Linear(hidden_size, hidden_size)
        self.value = nn.Linear(hidden_size, hidden_size)
        self.out = nn.Linear(hidden_size, hidden_size)

    def forward(self, x, mask=None):
        batch_size, seq_len, hidden_size = x.size()

        # Q, K, V 계산
        Q = self.query(x).view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
        K = self.key(x).view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
        V = self.value(x).view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)

        # Attention 스코어 계산
        scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.head_dim)

        if mask is not None:
            scores = scores.masked_fill(mask == 0, float('-inf'))

        attention_weights = torch.softmax(scores, dim=-1)

        # Value와 결합
        context = torch.matmul(attention_weights, V)
        context = context.transpose(1, 2).contiguous().view(batch_size, seq_len, hidden_size)

        return self.out(context)

3. BERT 사전학습 (Pre-training)

3.1 Masked Language Model (MLM)

BERT의 첫 번째 사전학습 태스크는 Masked Language Model입니다. 입력 토큰의 15%를 무작위로 선택하여 다음 규칙으로 처리합니다:

  • 80%: [MASK] 토큰으로 대체
  • 10%: 무작위 토큰으로 대체
  • 10%: 원래 토큰 유지
import random

def mask_tokens(tokens, tokenizer, mlm_probability=0.15):
    labels = tokens.clone()
    probability_matrix = torch.full(labels.shape, mlm_probability)

    # 특수 토큰은 마스킹하지 않음
    special_tokens_mask = tokenizer.get_special_tokens_mask(labels.tolist())
    probability_matrix.masked_fill_(torch.tensor(special_tokens_mask, dtype=torch.bool), value=0.0)

    masked_indices = torch.bernoulli(probability_matrix).bool()
    labels[~masked_indices] = -100  # 마스킹되지 않은 토큰은 손실 계산에서 제외

    # 80%는 [MASK]로 대체
    indices_replaced = torch.bernoulli(torch.full(labels.shape, 0.8)).bool() & masked_indices
    tokens[indices_replaced] = tokenizer.convert_tokens_to_ids('[MASK]')

    # 10%는 무작위 토큰으로 대체
    indices_random = torch.bernoulli(torch.full(labels.shape, 0.5)).bool() & masked_indices & ~indices_replaced
    random_words = torch.randint(len(tokenizer), labels.shape, dtype=torch.long)
    tokens[indices_random] = random_words[indices_random]

    # 나머지 10%는 그대로 유지
    return tokens, labels

3.2 Next Sentence Prediction (NSP)

두 번째 사전학습 태스크는 Next Sentence Prediction입니다. 두 문장이 주어졌을 때, 두 번째 문장이 실제로 첫 번째 문장 다음에 오는 문장인지 예측합니다.

# NSP 데이터 생성 예시
def create_nsp_data(document):
    examples = []
    for i in range(len(document) - 1):
        # 50% 확률로 실제 다음 문장 선택
        if random.random() < 0.5:
            sentence_a = document[i]
            sentence_b = document[i + 1]
            is_next = 1
        else:
            # 50% 확률로 무작위 문장 선택
            sentence_a = document[i]
            sentence_b = random.choice(all_sentences)
            is_next = 0

        examples.append({
            'sentence_a': sentence_a,
            'sentence_b': sentence_b,
            'is_next': is_next
        })
    return examples

3.3 학습 데이터 및 설정

BERT는 다음 데이터셋으로 학습되었습니다:

  • BooksCorpus: 800M 단어
  • English Wikipedia: 2,500M 단어 (리스트, 테이블, 헤더 제외)

학습 설정:

  • Batch Size: 256
  • Learning Rate: 1e-4 (Adam with warmup)
  • Training Steps: 1,000,000
  • 학습 시간: 4일 (TPU Pod 16개, BERT-Base 기준)

4. BERT Fine-tuning 전략

4.1 분류 태스크 (Classification)

감정 분석, 스팸 탐지 등의 분류 태스크에서는 [CLS] 토큰의 출력을 사용합니다.

from transformers import BertForSequenceClassification, BertTokenizer, AdamW
from torch.utils.data import DataLoader, Dataset
import torch

class SentimentDataset(Dataset):
    def __init__(self, texts, labels, tokenizer, max_length=128):
        self.encodings = tokenizer(texts, truncation=True, padding=True, max_length=max_length)
        self.labels = labels

    def __getitem__(self, idx):
        item = {key: torch.tensor(val[idx]) for key, val in self.encodings.items()}
        item['labels'] = torch.tensor(self.labels[idx])
        return item

    def __len__(self):
        return len(self.labels)

# 모델 및 토크나이저 로드
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
model = BertForSequenceClassification.from_pretrained('bert-base-uncased', num_labels=2)

# 데이터셋 준비
train_texts = ["This movie is great!", "I hate this film.", "Amazing performance!"]
train_labels = [1, 0, 1]  # 1: 긍정, 0: 부정

train_dataset = SentimentDataset(train_texts, train_labels, tokenizer)
train_loader = DataLoader(train_dataset, batch_size=8, shuffle=True)

# Fine-tuning
optimizer = AdamW(model.parameters(), lr=2e-5)
model.train()

for epoch in range(3):
    for batch in train_loader:
        optimizer.zero_grad()
        outputs = model(**batch)
        loss = outputs.loss
        loss.backward()
        optimizer.step()
        print(f"Loss: {loss.item():.4f}")

4.2 질의응답 (Question Answering)

SQuAD와 같은 질의응답 태스크에서는 답변의 시작과 끝 위치를 예측합니다.

from transformers import BertForQuestionAnswering, BertTokenizer
import torch

# QA 모델 로드
model = BertForQuestionAnswering.from_pretrained('bert-large-uncased-whole-word-masking-finetuned-squad')
tokenizer = BertTokenizer.from_pretrained('bert-large-uncased-whole-word-masking-finetuned-squad')

def answer_question(question, context):
    inputs = tokenizer(question, context, return_tensors='pt', max_length=512, truncation=True)

    with torch.no_grad():
        outputs = model(**inputs)

    # 시작과 끝 위치 찾기
    answer_start = torch.argmax(outputs.start_logits)
    answer_end = torch.argmax(outputs.end_logits) + 1

    # 토큰을 텍스트로 변환
    answer = tokenizer.convert_tokens_to_string(
        tokenizer.convert_ids_to_tokens(inputs['input_ids'][0][answer_start:answer_end])
    )
    return answer

# 사용 예시
context = """
BERT is a method of pre-training language representations, meaning that we train
a general-purpose "language understanding" model on a large text corpus, and then
use that model for downstream NLP tasks that we care about.
"""
question = "What is BERT?"
print(answer_question(question, context))
# 출력: "a method of pre-training language representations"

4.3 개체명 인식 (Named Entity Recognition)

from transformers import BertForTokenClassification, BertTokenizer
import torch

# NER 모델 로드
model = BertForTokenClassification.from_pretrained('dbmdz/bert-large-cased-finetuned-conll03-english')
tokenizer = BertTokenizer.from_pretrained('dbmdz/bert-large-cased-finetuned-conll03-english')

def extract_entities(text):
    inputs = tokenizer(text, return_tensors='pt', return_offsets_mapping=True)

    with torch.no_grad():
        outputs = model(**{k: v for k, v in inputs.items() if k != 'offset_mapping'})

    predictions = torch.argmax(outputs.logits, dim=2)
    tokens = tokenizer.convert_ids_to_tokens(inputs['input_ids'][0])

    # 라벨 매핑
    label_list = ['O', 'B-PER', 'I-PER', 'B-ORG', 'I-ORG', 'B-LOC', 'I-LOC', 'B-MISC', 'I-MISC']

    entities = []
    for token, pred in zip(tokens, predictions[0]):
        if token not in ['[CLS]', '[SEP]', '[PAD]']:
            label = label_list[pred]
            if label != 'O':
                entities.append((token, label))

    return entities

# 사용 예시
text = "Google was founded by Larry Page and Sergey Brin in California."
print(extract_entities(text))
# 출력: [('Google', 'B-ORG'), ('Larry', 'B-PER'), ('Page', 'I-PER'), ...]

5. BERT 변형 모델들

5.1 RoBERTa

Facebook AI가 개발한 RoBERTa는 BERT의 학습 방법을 최적화했습니다:

  • NSP 태스크 제거
  • 더 큰 배치 사이즈와 더 긴 학습
  • 동적 마스킹

5.2 ALBERT

"A Lite BERT"는 파라미터 효율성을 극대화했습니다:

  • 임베딩 팩터화: 큰 어휘 임베딩을 작은 행렬로 분해
  • 크로스-레이어 파라미터 공유

5.3 DistilBERT

BERT를 40% 더 작게 만들면서 97%의 성능을 유지:

  • Knowledge Distillation 기법 사용
  • 6개 레이어로 축소

5.4 한국어 BERT 모델

모델 개발사 특징
KoBERT SKT 한국어 위키 학습
KR-BERT ETRI 형태소 분석 기반
KoELECTRA Monologg ELECTRA 아키텍처
klue/bert-base KLUE KLUE 벤치마크 최적화
# 한국어 BERT 사용 예시
from transformers import BertModel, BertTokenizer

tokenizer = BertTokenizer.from_pretrained('klue/bert-base')
model = BertModel.from_pretrained('klue/bert-base')

text = "BERT는 자연어처리의 혁명입니다."
inputs = tokenizer(text, return_tensors='pt')
outputs = model(**inputs)

6. 실전 팁과 Best Practices

6.1 Fine-tuning 최적화

from transformers import get_linear_schedule_with_warmup

# 학습률 스케줄러 설정
num_training_steps = len(train_loader) * num_epochs
num_warmup_steps = num_training_steps // 10

scheduler = get_linear_schedule_with_warmup(
    optimizer,
    num_warmup_steps=num_warmup_steps,
    num_training_steps=num_training_steps
)

# 그래디언트 클리핑
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

6.2 메모리 최적화

# 그래디언트 축적으로 큰 배치 사이즈 시뮬레이션
accumulation_steps = 4
optimizer.zero_grad()

for i, batch in enumerate(train_loader):
    outputs = model(**batch)
    loss = outputs.loss / accumulation_steps
    loss.backward()

    if (i + 1) % accumulation_steps == 0:
        optimizer.step()
        optimizer.zero_grad()

# Mixed Precision Training
from torch.cuda.amp import autocast, GradScaler

scaler = GradScaler()
with autocast():
    outputs = model(**batch)
    loss = outputs.loss

scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()

6.3 성능 평가 메트릭

from sklearn.metrics import classification_report, confusion_matrix
import numpy as np

def evaluate_model(model, test_loader):
    model.eval()
    predictions, true_labels = [], []

    with torch.no_grad():
        for batch in test_loader:
            outputs = model(**batch)
            preds = torch.argmax(outputs.logits, dim=1)
            predictions.extend(preds.cpu().numpy())
            true_labels.extend(batch['labels'].cpu().numpy())

    print(classification_report(true_labels, predictions))
    print(confusion_matrix(true_labels, predictions))

7. 결론 및 향후 전망

BERT는 NLP 분야에 진정한 패러다임 전환을 가져왔습니다. 사전학습-미세조정(Pre-training and Fine-tuning) 패러다임은 이제 NLP의 표준이 되었으며, GPT, T5, BART 등 수많은 후속 모델들이 BERT의 영향을 받아 개발되었습니다.

핵심 정리

  1. 양방향 문맥 이해: BERT의 가장 큰 혁신
  2. 사전학습: MLM과 NSP를 통한 범용적 언어 이해 능력 학습
  3. Fine-tuning: 적은 데이터로도 뛰어난 성능 달성 가능
  4. 다양한 변형: 목적에 맞는 최적화된 모델 선택 가능

추가 학습 자료

BERT를 시작으로 NLP의 새로운 가능성을 탐험해보시기 바랍니다!