통계적 언어 모델은 현대 NLP의 기초입니다.
언어 모델(Language Model)은 단어 시퀀스의 확률을 계산하는 모델입니다.
"이 문장이 자연스러울 확률은 얼마나 될까?"
예시:
P(w₁, w₂, w₃, ..., wₙ) = ?
단어 시퀀스가 나타날 확률을 계산합니다.
문장의 확률을 단어별 조건부 확률의 곱으로 분해:
P(w₁, w₂, w₃, w₄) = P(w₁) × P(w₂|w₁) × P(w₃|w₁,w₂) × P(w₄|w₁,w₂,w₃)
P(나는, 밥을, 먹었다)
= P(나는) × P(밥을|나는) × P(먹었다|나는, 밥을)
긴 문맥을 모두 고려하면 계산이 불가능합니다! (데이터 부족)
해결책: 최근 N-1개의 단어만 고려하는 마르코프 가정(Markov Assumption)
이전 단어를 전혀 고려하지 않음:
P(w₁, w₂, w₃) = P(w₁) × P(w₂) × P(w₃)
학습 데이터:
나는 밥을 먹었다
나는 공부를 했다
너는 밥을 먹었다
단어 빈도 계산:
나는: 2번
너는: 1번
밥을: 2번
공부를: 1번
먹었다: 2번
했다: 1번
총 단어 수: 9개
확률 계산:
P(나는) = 2/9 ≈ 0.222
P(밥을) = 2/9 ≈ 0.222
P(먹었다) = 2/9 ≈ 0.222
문장 확률:
P(나는 밥을 먹었다) = P(나는) × P(밥을) × P(먹었다)
= 0.222 × 0.222 × 0.222
≈ 0.011
문제점: 단어 순서를 무시! "먹었다 밥을 나는"도 같은 확률 ❌
바로 이전 단어만 고려:
P(w₁, w₂, w₃) = P(w₁) × P(w₂|w₁) × P(w₃|w₂)
학습 데이터:
나는 밥을 먹었다
나는 공부를 했다
너는 밥을 먹었다
철수는 밥을 먹었다
Bigram 빈도 계산:
# "나는" 다음에 나오는 단어들
나는 → 밥을: 1번
나는 → 공부를: 1번
# "밥을" 다음에 나오는 단어들
밥을 → 먹었다: 3번
# "너는" 다음에 나오는 단어들
너는 → 밥을: 1번
조건부 확률 계산:
P(밥을|나는) = Count(나는 밥을) / Count(나는)
= 1/2 = 0.5
P(공부를|나는) = Count(나는 공부를) / Count(나는)
= 1/2 = 0.5
P(먹었다|밥을) = Count(밥을 먹었다) / Count(밥을)
= 3/3 = 1.0
문장 확률 계산:
P(나는 밥을 먹었다) = P(나는) × P(밥을|나는) × P(먹었다|밥을)
= 0.25 × 0.5 × 1.0
= 0.125
실제 비교:
# 자연스러운 문장
P(나는 밥을 먹었다) = 0.125 ✅
# 부자연스러운 문장
P(나는 먹었다 밥을) = P(나는) × P(먹었다|나는) × P(밥을|먹었다)
= 0.25 × 0 × ?
= 0 ❌
이전 2개 단어 고려:
P(w₃|w₁, w₂) = Count(w₁, w₂, w₃) / Count(w₁, w₂)
학습 데이터:
나는 오늘 밥을 먹었다
나는 어제 밥을 먹었다
너는 오늘 공부를 했다
나는 오늘 공부를 했다
Trigram 빈도:
나는 오늘 → 밥을: 1번
나는 오늘 → 공부를: 1번
나는 어제 → 밥을: 1번
오늘 밥을 → 먹었다: 1번
확률 계산:
P(밥을|나는, 오늘) = Count(나는 오늘 밥을) / Count(나는 오늘)
= 1/2 = 0.5
P(공부를|나는, 오늘) = Count(나는 오늘 공부를) / Count(나는 오늘)
= 1/2 = 0.5
P(먹었다|오늘, 밥을) = Count(오늘 밥을 먹었다) / Count(오늘 밥을)
= 1/1 = 1.0
문장 생성 예시:
시작: "나는 오늘"
다음 단어 후보:
- "밥을" (확률 0.5)
- "공부를" (확률 0.5)
"밥을" 선택 → "나는 오늘 밥을"
다음 단어 후보:
- "먹었다" (확률 1.0)
최종 문장: "나는 오늘 밥을 먹었다"
from collections import defaultdict, Counter
class BigramModel:
def __init__(self):
self.bigram_counts = defaultdict(Counter)
self.unigram_counts = Counter()
def train(self, sentences):
"""학습 데이터로 모델 훈련"""
for sentence in sentences:
words = ['<START>'] + sentence.split() + ['<END>']
for i in range(len(words) - 1):
# Bigram 카운트
self.bigram_counts[words[i]][words[i+1]] += 1
# Unigram 카운트
self.unigram_counts[words[i]] += 1
def probability(self, word, previous_word):
"""조건부 확률 P(word|previous_word) 계산"""
if self.unigram_counts[previous_word] == 0:
return 0.0
return (self.bigram_counts[previous_word][word] /
self.unigram_counts[previous_word])
def sentence_probability(self, sentence):
"""문장 전체의 확률 계산"""
words = ['<START>'] + sentence.split() + ['<END>']
prob = 1.0
for i in range(len(words) - 1):
p = self.probability(words[i+1], words[i])
if p == 0:
return 0.0
prob *= p
return prob
def generate(self, max_length=10):
"""문장 생성"""
import random
current_word = '<START>'
sentence = []
for _ in range(max_length):
# 다음 단어 후보들과 확률
candidates = self.bigram_counts[current_word]
if not candidates or current_word == '<END>':
break
# 확률에 따라 다음 단어 선택
words = list(candidates.keys())
weights = list(candidates.values())
next_word = random.choices(words, weights=weights)[0]
if next_word == '<END>':
break
sentence.append(next_word)
current_word = next_word
return ' '.join(sentence)
# 사용 예시
model = BigramModel()
# 학습
train_data = [
"나는 밥을 먹었다",
"나는 공부를 했다",
"너는 밥을 먹었다",
"철수는 공부를 했다",
"영희는 밥을 먹었다"
]
model.train(train_data)
# 확률 계산
print("=== 조건부 확률 ===")
print(f"P(밥을|나는) = {model.probability('밥을', '나는'):.3f}")
print(f"P(공부를|나는) = {model.probability('공부를', '나는'):.3f}")
print(f"P(먹었다|밥을) = {model.probability('먹었다', '밥을'):.3f}")
# 문장 확률
print("\n=== 문장 확률 ===")
print(f"P(나는 밥을 먹었다) = {model.sentence_probability('나는 밥을 먹었다'):.6f}")
print(f"P(나는 공부를 했다) = {model.sentence_probability('나는 공부를 했다'):.6f}")
# 문장 생성
print("\n=== 문장 생성 ===")
for i in range(3):
print(f"{i+1}. {model.generate()}")
출력 예시:
=== 조건부 확률 ===
P(밥을|나는) = 0.500
P(공부를|나는) = 0.500
P(먹었다|밥을) = 1.000
=== 문장 확률 ===
P(나는 밥을 먹었다) = 0.125000
P(나는 공부를 했다) = 0.125000
=== 문장 생성 ===
1. 나는 밥을 먹었다
2. 철수는 공부를 했다
3. 나는 공부를 했다
문제: 학습 데이터에 없는 조합은 확률이 0이 됨
해결: Laplace Smoothing (Add-one Smoothing)
class SmoothedBigramModel(BigramModel):
def __init__(self, vocabulary):
super().__init__()
self.vocabulary = vocabulary
self.vocab_size = len(vocabulary)
def probability(self, word, previous_word):
"""Laplace Smoothing 적용"""
numerator = self.bigram_counts[previous_word][word] + 1
denominator = self.unigram_counts[previous_word] + self.vocab_size
if denominator == self.vocab_size: # previous_word를 본 적 없음
return 1.0 / self.vocab_size
return numerator / denominator
# 사용 예시
vocabulary = set()
for sentence in train_data:
vocabulary.update(sentence.split())
vocabulary.update(['<START>', '<END>'])
smoothed_model = SmoothedBigramModel(vocabulary)
smoothed_model.train(train_data)
print("=== 스무딩 적용 ===")
# 학습 데이터에 없던 조합
print(f"P(피자를|나는) = {smoothed_model.probability('피자를', '나는'):.6f}")
print(f"(원래는 0이지만 스무딩으로 작은 확률 부여)")
class SentenceCompletion:
def __init__(self, model):
self.model = model
def complete(self, prefix, n_suggestions=3):
"""문장 완성 제안"""
words = prefix.split()
if not words:
return []
last_word = words[-1]
candidates = self.model.bigram_counts[last_word]
# 확률 높은 순으로 정렬
sorted_candidates = sorted(
candidates.items(),
key=lambda x: x[1],
reverse=True
)
suggestions = []
for word, count in sorted_candidates[:n_suggestions]:
if word != '<END>':
prob = self.model.probability(word, last_word)
suggestions.append((word, prob))
return suggestions
# 사용
completion = SentenceCompletion(model)
print("=== 문장 자동 완성 ===")
prefix = "나는"
suggestions = completion.complete(prefix)
print(f"입력: '{prefix}'")
print("제안:")
for word, prob in suggestions:
print(f" - {word} (확률: {prob:.3f})")
출력:
=== 문장 자동 완성 ===
입력: '나는'
제안:
- 밥을 (확률: 0.500)
- 공부를 (확률: 0.500)
✅ 간단하고 직관적: 이해하기 쉬움
✅ 빠른 학습: 단순 카운팅만 필요
✅ 효율적 추론: 테이블 룩업만 필요
✅ 해석 가능: 확률을 직접 볼 수 있음
❌ 희소성 문제(Sparsity): 학습 데이터에 없는 조합은 확률 0
❌ 짧은 문맥: N이 작으면 긴 의존성 포착 불가
❌ 큰 N의 문제: N이 크면 필요한 데이터 기하급수적 증가
❌ 일반화 부족: 비슷한 단어를 다르게 취급
Perplexity는 언어 모델의 성능을 측정하는 지표입니다.
PPL = 2^(-1/N × Σ log₂ P(wᵢ|context))
의미: 모델이 다음 단어를 예측할 때 평균적으로 몇 개의 선택지를 고려하는가?
import math
def perplexity(model, test_sentences):
"""Perplexity 계산"""
log_prob_sum = 0
word_count = 0
for sentence in test_sentences:
words = ['<START>'] + sentence.split() + ['<END>']
for i in range(len(words) - 1):
prob = model.probability(words[i+1], words[i])
if prob > 0:
log_prob_sum += math.log2(prob)
word_count += 1
return 2 ** (-log_prob_sum / word_count)
# 평가
test_data = [
"나는 밥을 먹었다",
"너는 공부를 했다"
]
ppl = perplexity(model, test_data)
print(f"Perplexity: {ppl:.2f}")
print("(낮을수록 좋은 모델)")
해석:
| 특징 | N-gram (SLM) | Neural LM | Transformer |
|---|---|---|---|
| 문맥 길이 | 고정 (N-1) | 가변 (RNN) | 매우 긴 (Attention) |
| 파라미터 | 테이블 크기 | 수백만~수억 | 수십억~수조 |
| 학습 속도 | 빠름 | 중간 | 느림 |
| 성능 | 낮음 | 중간 | 높음 |
| 메모리 | 큼 (희소) | 작음 (밀집) | 중간 |
| 예시 | - | LSTM LM | GPT, BERT |
def speech_recognition_example():
"""음성 인식 후보 선택"""
# 음향 모델이 제안한 후보들
acoustic_candidates = [
("나는 인정", 0.6), # 음향적 확률
("나는 인생", 0.4)
]
# 언어 모델 확률
lm_probs = {
"나는 인정": model.sentence_probability("나는 인정"),
"나는 인생": model.sentence_probability("나는 인생")
}
# 결합 (가중 평균)
alpha = 0.7 # 언어 모델 가중치
for candidate, acoustic_prob in acoustic_candidates:
lm_prob = lm_probs[candidate]
combined = (acoustic_prob ** (1-alpha)) * (lm_prob ** alpha)
print(f"{candidate}: {combined:.6f}")
# 실행
speech_recognition_example()