CTC (음성 인식)
더 많은 작업
- Connectionist Temporal Classification, CTC; CTC
- 입력 프레임과 출력 글자 사이의 정렬 정보 없이 시퀀스 레이블링 신경망을 학습시키는 손실 함수와 출력 표현
CTC(Connectionist Temporal Classification)는 길이 T의 입력(음성 프레임 열)과 그보다 짧은 길이 U의 정답(글자 열)이 주어졌지만, 어느 프레임이 어느 글자에 해당하는지는 모를 때 신경망을 학습시키는 방법이다. 2006년 알렉스 그레이브스(Alex Graves) 등이 ICML에서 발표했다.[1] 프레임마다 글자 또는 빈칸(blank)의 확률을 내게 하고, 정답 글자 열로 접힐 수 있는 모든 정렬의 확률을 동적 계획법으로 더해 그 음의 로그를 손실로 쓴다.
음성 인식 문서의 종단간 모델 비교에서 CTC는 한 줄로 소개되어 있다. 이 문서는 확률 모델, 전방-후방 알고리즘, 기울기, 디코딩, PyTorch 구현을 다룬다. CTC는 음성 인식 외에도 필기 인식, 광학 문자 인식(줄 단위 문자 인식)처럼 입력이 출력보다 길고 순서가 단조롭게 대응하는 문제에 쓰인다.
"hello"라고 말한 1초짜리 음성을 10ms 간격으로 자르면 프레임이 100개쯤 된다. 정답은 글자 5개뿐이고, 각 글자가 몇 번째 프레임부터 몇 번째까지인지는 레이블에 없다. 기존 방식은 은닉 마르코프 모델(HMM)과 결합해 강제 정렬(forced alignment)로 프레임별 정답을 먼저 만든 뒤 신경망을 프레임 분류기로 학습시켰다. CTC는 정렬을 숨은 변수로 두고 가능한 모든 정렬에 대해 합산하므로, 음성과 전사문 쌍만으로 바로 학습할 수 있다. 원 논문은 이를 "HMM 없이 RNN만으로 분할되지 않은 열에 레이블을 붙이는" 방법으로 제시했다.[1]
글자 집합 에 빈칸 기호 (이하 블랭크)를 더한 에 대해, 신경망은 프레임 t마다 소프트맥스 분포 를 낸다. 프레임마다 기호 하나씩 고른 길이 T의 열 를 경로(path)라 하며, 프레임 사이의 조건부 독립을 가정해
로 둔다. 접기 함수 는 경로에서 (1) 연속으로 반복된 기호를 하나로 합치고 (2) 블랭크를 지운다. 예를 들어 다. 정답 열 의 확률은 그렇게 접히는 모든 경로의 합이다.
블랭크가 필요한 이유는 두 가지다.
- 반복 글자 구분: "hello"의 "ll"은 반복을 합치는 규칙 때문에 "l l" 사이에 블랭크가 하나 이상 있어야만 두 글자로 남는다.
- 출력 없음: 묵음이나 글자와 글자 사이의 전이 구간처럼 어느 글자도 내지 않을 프레임에 블랭크를 쓴다.
같은 이유로 입력 길이는 정답 길이에 연속 반복 글자 수를 더한 것보다 짧을 수 없다. 이 조건을 어기면 가능한 경로가 없어 확률이 0, 손실이 무한대가 된다.
경로 수는 T에 대해 지수적으로 많으므로 직접 더할 수 없다. HMM의 전방-후방 알고리즘(forward-backward algorithm)과 같은 방식으로 동적 계획법을 쓴다.[1]
정답 앞뒤와 글자 사이에 블랭크를 끼운 확장 열 을 만든다. 길이는 이다. 전방 변수 를 "처음 t개 프레임이 의 앞 s개 기호까지를 만들고 t번째 프레임이 인 경로들의 확률 합"으로 정의한다.
초기값은 블랭크 또는 첫 글자로만 시작할 수 있으므로 , , 나머지는 0이다. 점화식은
이다. 같은 위치에 머무르거나(같은 기호 반복), 한 칸 나아가거나, 블랭크를 건너뛰어 두 칸 나아갈 수 있다. 다만 현재 기호가 블랭크이거나 두 칸 앞과 같은 글자(반복 글자)이면 건너뛸 수 없다. 반복 글자 사이의 블랭크는 생략할 수 없기 때문이다. 끝은 마지막 글자나 마지막 블랭크 어느 쪽이어도 되므로
이다. 후방 변수 ("t번째 프레임이 일 때 t+1번째 이후 프레임이 나머지를 완성할 확률")를 거꾸로 같은 방식으로 계산하면, 아무 t에서나 가 성립한다. 시간·공간 복잡도는 다. 실제 구현은 언더플로를 막기 위해 로그 공간에서 로 더한다.
소프트맥스 직전 로짓 에 대한 손실의 기울기는
이다(위의 β 정의 기준). 둘째 항은 "정답이 주어졌을 때 t번째 프레임이 기호 k일 사후 확률"이다. 즉 CTC 학습은 모든 정렬에 대한 기대값으로 만든 부드러운 프레임 레이블과 모델 출력의 차이를 줄이는 것과 같다. 이 사후 확률이 가장 큰 정렬을 뽑으면 학습이 끝난 모델로 강제 정렬도 할 수 있다.
학습된 CTC 모델은 대부분의 프레임에서 블랭크 확률이 1에 가깝고, 글자는 한두 프레임에서만 뾰족하게(peaky) 튀어나오는 경향이 있다. 체이어(Zeyer) 등은 이 현상이 CTC 손실의 기울기 하강 수렴 특성에서 생긴다는 것을 형식적으로 분석하고, 경우에 따라 학습에 불리하다는 것을 보였다.[2] 그래서 CTC 출력의 글자 위치를 실제 발화 시각으로 그대로 쓰면 어긋날 수 있다.
학습한 모델로 가장 그럴듯한 글자 열 를 정확히 찾는 것은 어렵다. 경로 합이므로 확률이 가장 큰 경로가 확률이 가장 큰 글자 열과 일치한다는 보장이 없다.
- 최선 경로(best path, 탐욕) 디코딩: 프레임마다 argmax 기호를 고른 뒤 로 접는다. 병렬로 한 번에 끝나 가장 빠르다.
- 접두사 탐색(prefix search) 디코딩: 원 논문이 제안한 방식으로, 같은 글자 열로 접히는 경로들의 확률을 합쳐 가며 접두사를 넓혀 간다.[1]
- 접두사 빔 서치와 언어 모델: 상위 몇 개 접두사만 유지하면서 n-gram이나 신경망 언어 모델 점수를 더한다(얕은 융합). CTC는 출력 사이의 의존성을 모델링하지 않으므로 외부 언어 모델의 이득이 크다. 언어 모델 디코딩의 빔 서치와 원리가 같다.
- WFST 디코딩: 발음 사전과 언어 모델을 가중 유한 상태 변환기로 합성해 탐색한다. 전통적 음성 인식 디코더를 재활용하는 방식이다.
원 논문의 TIMIT 음소 인식 실험에서 레이블 오류율은 문맥 독립 HMM 38.85%, 문맥 종속 HMM 35.21%, BLSTM/HMM 혼합 33.84%였고, CTC는 최선 경로 31.47%, 접두사 탐색 30.51%였다.[1]
PyTorch의 torch.nn.CTCLoss(blank=0, reduction='mean', zero_infinity=False)는 다음 입력을 받는다(PyTorch 2.14 문서 기준).[3]
log_probs: (T, N, C) 모양의 로그 확률. 배치 차원이 두 번째다.log_softmax출력을 넣는다.targets: (N, S)로 패딩한 정답 또는 모든 정답을 이어 붙인 1차원 텐서. 블랭크 번호를 포함하면 안 된다.input_lengths,target_lengths: 샘플별 실제 길이.
zero_infinity=True로 두면 입력이 너무 짧아 정렬이 불가능한 샘플의 무한대 손실과 기울기를 0으로 만든다. cuDNN 구현은 정답이 이어 붙인 형식이고, 모든 입력 길이가 T이며, blank=0, 정답 길이 256 이하, 정수 인자가 int32, log_probs가 float32일 때만 쓰인다.[3]
import torch
import torch.nn as nn
T, N, C = 50, 4, 30 # 프레임 수, 배치, 클래스 수(0번 = 블랭크)
logits = torch.randn(N, T, C, requires_grad=True) # 인코더 출력 대신
log_probs = logits.log_softmax(-1).transpose(0, 1) # (T, N, C)
targets = torch.randint(1, C, (N, 12), dtype=torch.long) # 블랭크(0) 제외
input_lengths = torch.full((N,), T, dtype=torch.long)
target_lengths = torch.tensor([12, 10, 8, 12], dtype=torch.long)
ctc = nn.CTCLoss(blank=0, zero_infinity=True)
loss = ctc(log_probs, targets, input_lengths, target_lengths)
loss.backward()
위 점화식을 로그 공간에서 그대로 옮긴 것이다. 같은 입력에서 torch.nn.CTCLoss(reduction='sum')과 같은 값(13.9608)이 나오는 것을 확인했다(PyTorch 2.5).
import numpy as np
def ctc_loss(log_probs, target, blank=0):
"""log_probs: (T, C) 로그 확률, target: 블랭크 없는 정답 번호 리스트"""
T = log_probs.shape[0]
ext = [blank]
for c in target:
ext += [c, blank] # l' 길이 2U+1
S = len(ext)
alpha = np.full((T, S), -np.inf)
alpha[0, 0] = log_probs[0, ext[0]]
alpha[0, 1] = log_probs[0, ext[1]]
for t in range(1, T):
for s in range(S):
a = alpha[t - 1, s]
if s >= 1:
a = np.logaddexp(a, alpha[t - 1, s - 1])
if s >= 2 and ext[s] != blank and ext[s] != ext[s - 2]:
a = np.logaddexp(a, alpha[t - 1, s - 2]) # 블랭크 건너뛰기
alpha[t, s] = a + log_probs[t, ext[s]]
return -np.logaddexp(alpha[T - 1, S - 1], alpha[T - 1, S - 2])
def greedy_decode(log_probs, blank=0):
best = log_probs.argmax(-1).tolist()
out, prev = [], None
for k in best:
if k != prev and k != blank: # 반복 합치기 + 블랭크 제거
out.append(k)
prev = k
return out
- Deep Speech(2014): 바이두는 CTC로 학습한 양방향 RNN으로 음소 사전이나 "음소" 개념 없이 종단간 음성 인식을 구현해 Switchboard Hub5'00 전체 테스트에서 16.0% 오류율을 보고했다.[4] 후속 Deep Speech 2(2015)는 같은 접근을 영어와 중국어로 넓혔다.[5]
- wav2vec 2.0(2020): 레이블 없는 음성으로 자기 지도 학습한 인코더 위에 선형 층을 얹고 CTC로 미세 조정했다. 53,000시간의 무레이블 음성으로 사전 학습하고 레이블 10분만으로 LibriSpeech clean/other 단어 오류율 4.8/8.2%를 냈다.[6] 허깅 페이스 Transformers의
Wav2Vec2ForCTC가 이 구조다. - CTC-어텐션 결합: 김수연, 호리 다카아키, 와타나베 신지는 어텐션 인코더-디코더에 CTC 손실을 보조 과제로 함께 걸어, CTC의 단조 정렬 제약이 어텐션의 정렬 학습을 돕게 했다. WSJ와 CHiME-4에서 두 단독 모델보다 글자 오류율을 상대적으로 5.4~14.6% 줄였다.[7]
- RNN-T: CTC에 이전 출력을 입력받는 예측 신경망을 더해 출력 사이의 의존성을 모델링한 것이다.[8]
| 방식 | 출력 의존성 | 정렬 | 추론 | 스트리밍 | 대표 사례 |
|---|---|---|---|---|---|
| CTC | 없음(프레임 조건부 독립) | 단조, 블랭크로 표현 | 한 번에 병렬, 가장 빠름 | 쉬움 | Deep Speech, wav2vec 2.0 미세 조정 |
| RNN-T | 이전 출력에 조건화 | 단조 | 프레임·출력 단위 순차 | 쉬움 | 스트리밍 음성 인식 |
| 어텐션 인코더-디코더 | 자기회귀 | 학습으로 결정, 단조 보장 없음 | 출력 길이만큼 순차 | 어려움 | Whisper 같은 대규모 모델 |
| CTC + 어텐션 | 디코더가 담당 | CTC가 단조성 보조 | 빔 서치에서 두 점수 결합 | 구성에 따라 | 김수연 등(2016)의 결합 학습 |
- 조건부 독립: 프레임 출력끼리 서로를 보지 않으므로 "철자는 그럴듯하지만 말이 안 되는" 출력이 나오기 쉽다. 외부 언어 모델이나 큰 사전 학습 인코더로 보완한다.
- 단조 정렬: 입력과 출력의 순서가 같다고 가정하므로 어순이 바뀌는 기계 번역에는 맞지 않는다.
- 길이 조건: 출력 단위가 너무 잘거나(글자) 입력이 너무 짧게 줄어들면(큰 다운샘플링) 정렬 불가능한 샘플이 생긴다. 한국어처럼 음절 종류가 많은 언어는 음절, 자모, 하위 단어 중 출력 단위를 정하는 것이 설계 문제가 된다.
- 시각 정보 부정확: 뾰족한 출력 때문에 글자별 시각이 실제 발화 구간과 어긋날 수 있다.[2]
- ↑ 1.0 1.1 1.2 1.3 1.4 Alex Graves, Santiago Fernández, Faustino Gomez, Jürgen Schmidhuber, 〈Connectionist Temporal Classification: Labelling Unsegmented Sequence Data with Recurrent Neural Networks〉, ICML 2006. PDF
- ↑ 2.0 2.1 Albert Zeyer, Ralf Schlüter, Hermann Ney, 〈Why does CTC result in peaky behavior?〉, 2021. arXiv:2105.14849
- ↑ 3.0 3.1 PyTorch 문서, torch.nn.CTCLoss
- ↑ Awni Hannun 외, 〈Deep Speech: Scaling up end-to-end speech recognition〉, 2014. arXiv:1412.5567
- ↑ Dario Amodei 외, 〈Deep Speech 2: End-to-End Speech Recognition in English and Mandarin〉, 2015. arXiv:1512.02595
- ↑ Alexei Baevski 외, 〈wav2vec 2.0: A Framework for Self-Supervised Learning of Speech Representations〉, NeurIPS 2020. arXiv:2006.11477
- ↑ Suyoun Kim, Takaaki Hori, Shinji Watanabe, 〈Joint CTC-Attention based End-to-End Speech Recognition using Multi-task Learning〉, 2016. arXiv:1609.06773
- ↑ Alex Graves, 〈Sequence Transduction with Recurrent Neural Networks〉, 2012. arXiv:1211.3711