한양과학기술고등학교

에듀테크 프로젝트 대시보드

비터비 알고리즘 (Viterbi Algorithm)

친구가 매일 뭘 했는지(산책 · 쇼핑 · 청소)만 메신저로 알려준다. 그런데 그 동네 날씨(☀️ 맑음 / ☔ 비)는 직접 볼 수 없다. "행동 기록만 보고, 지난 며칠간 날씨가 어땠을지 가장 그럴듯한 시퀀스를 통째로 맞혀라." 이게 은닉 마르코프 모델(HMM)의 대표 문제이고, 이를 푸는 O(T·N²) 동적 계획법이 비터비 알고리즘이다.

비터비는 1967년 통신용 오류정정 부호를 디코딩하려고 나왔지만, 지금은 스마트폰 자동완성·음성 인식· GPS 경로 맵매칭·유전자 예측·통신 모뎀 어디에나 들어가 있다.

1. 은닉 마르코프 모델의 3가지 확률

HMM 은 "숨은 상태가 마르코프 사슬로 흐르고, 각 상태가 관측을 확률적으로 방출한다"는 모델이다. 필요한 건 딱 3개의 표다.

① 시작 확률 π — 첫날 날씨

맑음비
0.60.4

② 전이 확률 A — 어제 날씨 → 오늘 날씨

from \ to맑음비
맑음0.70.3
비0.40.6

③ 방출 확률 B — 그날 날씨 → 친구의 행동

상태 \ 관측🚶 산책🛍️ 쇼핑🧹 청소
맑음0.60.30.1
비0.10.40.5

각 행의 합은 1 이다. "비 오는 날엔 청소할 확률이 높다"처럼 상식이 숫자로 들어가 있다.

2. 그리디는 왜 틀리나

"매일 그 행동을 가장 잘 설명하는 날씨를 하나씩 고르면 되지 않나?" — 안 된다. 그렇게 지역적으로 고르면 날씨끼리의 연결 확률(전이 A) 을 무시하게 된다. 어제·오늘·내일을 한 덩어리로 봐야 진짜 최적 경로가 나온다. 아래 데모에서 그리디 경로와 비터비 경로가 갈라지는 걸 직접 볼 수 있다.

3. 점화식 — 최댓값을 이어 붙인다

δ_t(j) = "시각 t 에 상태 j 로 끝나는 경로 중 가장 확률 높은 경로의 확률".

δt(j) = maxi [ δt−1(i) · Ai→j ] · Bj(ot)
시작: δ1(j) = πj · Bj(o1)   |   역추적: ψt(j) = argmaxi [ δt−1(i) · Ai→j ]

각 노드는 "이전 열의 어느 노드에서 왔을 때 가장 좋은가"만 기억한다(이게 백포인터 ψ). 마지막 열에서 δ 가 최대인 노드를 찾고, ψ 를 거꾸로 따라가면 전체 최적 상태열이 복원된다.

4. 트렐리스에서 직접 돌려보기

관측 시퀀스
후보 전이 (계산됨) 각 노드의 최선 백포인터 ψ 비터비 최적 경로 그리디 경로 (열별 최댓값)

※ δ 값은 확률의 곱이라 열이 늘수록 빠르게 작아진다. 실제 구현은 log 를 취해 덧셈으로 바꾼다 — 위 체크박스를 켜면 log₂ δ (음수, 클수록 좋음)로 표시된다.

5. 코드로 보기

import math

def viterbi(obs, states, start_p, trans_p, emit_p):
    # log 공간: 곱 → 합, 언더플로 방지
    L = lambda p: math.log(p) if p > 0 else -1e18

    delta = [{}]           # delta[t][state] = 최선 로그확률
    psi   = [{}]           # psi[t][state]   = 직전 상태(백포인터)
    for s in states:                                 # t = 0 초기화
        delta[0][s] = L(start_p[s]) + L(emit_p[s][obs[0]])
        psi[0][s] = None

    for t in range(1, len(obs)):
        delta.append({}); psi.append({})
        for j in states:
            best_i, best_v = None, -1e18
            for i in states:                          # 이전 열 N개 후보
                v = delta[t-1][i] + L(trans_p[i][j])
                if v > best_v:
                    best_i, best_v = i, v
            delta[t][j] = best_v + L(emit_p[j][obs[t]])
            psi[t][j]   = best_i                       # 어디서 왔는지 기록

    last = max(states, key=lambda s: delta[-1][s])   # 종료: 최댓값 상태
    path = [last]
    for t in range(len(obs)-1, 0, -1):              # 역추적
        path.append(psi[t][path[-1]])
    return path[::-1], delta[-1][last]

바깥 루프 T (관측 길이) × 안쪽 이중 루프 N² (상태 쌍) = O(T·N²) 시간, O(T·N) 공간. 모든 경우의 수 NT 를 지수 → 선형으로 접는다.

6. 어디에 쓰이나

7. 한 줄 요약

비터비는 "열마다 각 상태로 끝나는 최고의 경로 하나만 남기고, 어디서 왔는지 화살표를 남긴다"는 동적 계획법이다. 관측이 끝나면 최댓값에서 화살표를 거꾸로 따라가 전역 최적 상태열을 한 번에 얻는다 — 그리디로는 절대 못 얻는 답을, 지수 시간 대신 O(T·N²) 에.