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은 다음과 같이 작동합니다:
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의 영향을 받아 개발되었습니다.
핵심 정리
- 양방향 문맥 이해: BERT의 가장 큰 혁신
- 사전학습: MLM과 NSP를 통한 범용적 언어 이해 능력 학습
- Fine-tuning: 적은 데이터로도 뛰어난 성능 달성 가능
- 다양한 변형: 목적에 맞는 최적화된 모델 선택 가능
추가 학습 자료
- BERT 원논문 - Devlin et al., 2018
- Hugging Face Transformers 문서
- BERT 시각화 도구
- The Illustrated BERT
BERT를 시작으로 NLP의 새로운 가능성을 탐험해보시기 바랍니다!

