통계적 언어 모델 (Statistical Language Model, SLM)

김동준·2025년 10월 17일

통계적 언어 모델 (Statistical Language Model, SLM) 완벽 가이드

통계적 언어 모델은 현대 NLP의 기초입니다.

1. 통계적 언어 모델이란?

언어 모델(Language Model)은 단어 시퀀스의 확률을 계산하는 모델입니다.

핵심 질문:

"이 문장이 자연스러울 확률은 얼마나 될까?"

예시:

  • "나는 밥을 먹었다" → 확률 높음 ✅
  • "나는 밥을 먹었다는" → 확률 낮음 ❌
  • "밥을 나는 먹었다" → 확률 중간 (비문법적이지만 이해 가능)

수학적 정의:

P(w₁, w₂, w₃, ..., wₙ) = ?

단어 시퀀스가 나타날 확률을 계산합니다.

2. 왜 필요할까?

응용 분야:

  1. 기계 번역: "I love you" → "나는 너를 사랑해" vs "나는 사랑해 너를"
  2. 음성 인식: "인정" vs "인생" (발음이 비슷할 때)
  3. 자동 완성: "오늘 날씨가" → "좋다" vs "나쁘다"
  4. 맞춤법 검사: "먹엇다" → "먹었다"

3. 확률 계산 방법

Chain Rule (연쇄 법칙)

문장의 확률을 단어별 조건부 확률의 곱으로 분해:

P(w₁, w₂, w₃, w₄) = P(w₁) × P(w₂|w₁) × P(w₃|w₁,w₂) × P(w₄|w₁,w₂,w₃)

예시: "나는 밥을 먹었다"

P(나는, 밥을, 먹었다) 
= P(나는) × P(밥을|나는) × P(먹었다|나는, 밥을)

문제점:

긴 문맥을 모두 고려하면 계산이 불가능합니다! (데이터 부족)

4. N-gram 모델 (핵심 개념!)

해결책: 최근 N-1개의 단어만 고려하는 마르코프 가정(Markov Assumption)

Unigram (1-gram)

이전 단어를 전혀 고려하지 않음:

P(w₁, w₂, w₃) = P(w₁) × P(w₂) × P(w₃)

예시 1: Unigram 모델

학습 데이터:

나는 밥을 먹었다
나는 공부를 했다
너는 밥을 먹었다

단어 빈도 계산:

나는: 2번
너는: 1번
밥을: 2번
공부를: 1번
먹었다: 2번
했다: 1번
총 단어 수: 9개

확률 계산:

P(나는) = 2/90.222
P(밥을) = 2/90.222
P(먹었다) = 2/90.222

문장 확률:

P(나는 밥을 먹었다) = P(나는) × P(밥을) × P(먹었다)
                    = 0.222 × 0.222 × 0.222
                    ≈ 0.011

문제점: 단어 순서를 무시! "먹었다 밥을 나는"도 같은 확률 ❌

Bigram (2-gram)

바로 이전 단어만 고려:

P(w₁, w₂, w₃) = P(w₁) × P(w₂|w₁) × P(w₃|w₂)

예시 2: Bigram 모델

학습 데이터:

나는 밥을 먹었다
나는 공부를 했다
너는 밥을 먹었다
철수는 밥을 먹었다

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

Trigram (3-gram)

이전 2개 단어 고려:

P(w₃|w₁, w₂) = Count(w₁, w₂, w₃) / Count(w₁, w₂)

예시 3: Trigram 모델

학습 데이터:

나는 오늘 밥을 먹었다
나는 어제 밥을 먹었다
너는 오늘 공부를 했다
나는 오늘 공부를 했다

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)

최종 문장: "나는 오늘 밥을 먹었다"

5. 실전 Python 구현

예시 1: Bigram 모델 구현

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. 나는 공부를 했다

예시 2: 스무딩 (Smoothing) 적용

문제: 학습 데이터에 없는 조합은 확률이 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이지만 스무딩으로 작은 확률 부여)")

예시 3: 실제 응용 - 문장 완성

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)

6. N-gram 모델의 장단점

장점:

간단하고 직관적: 이해하기 쉬움
빠른 학습: 단순 카운팅만 필요
효율적 추론: 테이블 룩업만 필요
해석 가능: 확률을 직접 볼 수 있음

단점:

희소성 문제(Sparsity): 학습 데이터에 없는 조합은 확률 0
짧은 문맥: N이 작으면 긴 의존성 포착 불가
큰 N의 문제: N이 크면 필요한 데이터 기하급수적 증가
일반화 부족: 비슷한 단어를 다르게 취급

7. Perplexity (복잡도) - 모델 평가

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("(낮을수록 좋은 모델)")

해석:

  • Perplexity = 10: 평균 10개 단어 중 하나 선택
  • Perplexity = 100: 평균 100개 단어 중 하나 선택
  • 낮을수록 좋음!

8. 현대 언어 모델과의 비교

특징N-gram (SLM)Neural LMTransformer
문맥 길이고정 (N-1)가변 (RNN)매우 긴 (Attention)
파라미터테이블 크기수백만~수억수십억~수조
학습 속도빠름중간느림
성능낮음중간높음
메모리큼 (희소)작음 (밀집)중간
예시-LSTM LMGPT, BERT

9. 실전 활용 예시

음성 인식에서의 활용

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()

10. 핵심 요약

통계적 언어 모델 (SLM)이란?

  1. 단어 시퀀스의 확률을 계산하는 모델
  2. 과거 학습 데이터의 빈도를 기반으로 동작
  3. N-gram이 가장 기본적인 방법

N-gram 모델

  1. Unigram: 단어 순서 무시 (가장 단순)
  2. Bigram: 바로 이전 단어만 고려 (가장 많이 사용)
  3. Trigram: 이전 2개 단어 고려 (더 정확하지만 데이터 많이 필요)

주요 개념

  • 희소성 문제: 스무딩으로 해결
  • Perplexity: 모델 성능 평가 지표 (낮을수록 좋음)
  • 응용: 기계 번역, 음성 인식, 자동 완성, 맞춤법 검사

현대적 발전

  • N-gram → Neural Language Model → Transformer
  • GPT, BERT 등은 SLM의 발전된 형태
  • 하지만 기본 원리는 동일: "다음 단어의 확률 계산"
profile
Story Engineer

0개의 댓글