지난 글에서 문서에서 정답 스팬을 찾는 질의응답을 다뤘다. 거기서 계속 물었던 것이 「입력의 어느 부분이 출력의 어느 부분에 대응하는가」였는데, 기계 번역은 그 물음이 처음 제기된 자리다. 어텐션이라는 장치 자체가 번역에서 나왔고, Transformer를 소개한 논문도 번역 논문이었다.
번역은 겉보기에 단순하다. 「Hello」를 「안녕하세요」로 바꾸면 된다. 그런데 문장이 조금만 길어지면 어순이 뒤집히고, 한국어 쪽에서는 경어 등급을 골라야 하고, 앞 문장에 나온 대명사가 무엇을 가리키는지 알아야 다음 문장을 옮길 수 있다. 이 글은 그 각각을 모델의 어느 부분이 감당하는지, 그리고 감당하지 못하는 것을 실무에서 어떻게 메우는지를 따라간다.
번역 모델의 세대
규칙 기반
1950년대부터 1980년대까지는 언어학자가 직접 쓴 문법 규칙과 이중언어 사전을 썼다. 소스 문장을 구문 분석해 트리를 만들고, 변환 규칙으로 타겟 언어의 트리로 바꾸고, 거기서 문장을 생성하는 세 단계 구조다.
정확도는 규칙이 덮는 범위 안에서 아주 높다. 문제는 그 범위를 넓히는 비용이다. 규칙을 하나 더하면 기존 규칙과 충돌하는 자리가 생기고, 언어 쌍마다 규칙을 새로 써야 한다. 지금도 어휘와 문형이 고정된 좁은 도메인, 이를테면 일기예보나 법령 조문의 정형 표현에서는 남아 있다.
통계 기반과 정렬
1990년대에 방향이 바뀐다. 규칙을 쓰는 대신 사람이 번역해 둔 문장 쌍을 대량으로 모아 놓고, 거기서 「어느 단어가 어느 단어로 번역되는가」의 확률을 세는 방식이다.
여기서 나온 핵심 개념이 정렬이다. 「나는 학교에 간다」와 「I go to school」을 나란히 놓았을 때 「학교」가 「school」에 대응한다는 것을 아무도 알려 주지 않았는데도, 같은 짝이 수백만 번 나오면 통계가 그 대응을 찾아낸다. IBM 모델 1~5가 이 정렬을 점점 정교하게 다듬었고, 이후 구절 기반 통계 번역은 단어가 아니라 여러 단어 덩어리 단위로 정렬을 잡았다. 구글 번역이 2006년 서비스를 시작할 때 쓴 것이 이 방식이고, 2016년까지 유지했다.
약점은 긴 의존성이었다. 문장 끝의 동사가 문장 앞의 주어를 결정하는 구조에서 구절 단위 모델은 그 둘을 이어 볼 방법이 없다.
신경망 기반
2014년부터 순환 신경망 기반 인코더-디코더가 등장한다. 소스 문장 전체를 벡터 하나로 압축했다가 거기서 타겟 문장을 풀어내는 구조인데, 문장이 길어지면 그 벡터 하나에 다 안 담긴다는 병목이 바로 드러났다. 어텐션은 그 병목을 풀려고 나온 장치다 — 디코더가 토큰을 하나 낼 때마다 소스의 모든 위치를 다시 보고 필요한 자리에 가중치를 몰아주게 한다.
구글이 2016년 자사 번역을 순환 신경망 기반 신경망 번역으로 바꾸면서 품질이 한 단계 올라갔고, 2017년 Transformer가 순환 구조를 아예 걷어내고 어텐션만 남기면서 지금의 구조가 됐다. 현재 상용 서비스는 전부 이 계열 위에 있다.
크로스 어텐션
인코더와 디코더
인코더는 소스 문장의 모든 토큰을 한 번에 처리해 각 위치마다 문맥이 담긴 벡터를 만든다. 셀프 어텐션이 쓰이므로 어느 토큰이든 문장 안의 다른 모든 토큰을 참조할 수 있고, 앞뒤 구분이 없다.
디코더는 타겟 토큰을 하나씩 자동회귀로 생성한다. 여기서는 셀프 어텐션에 마스크가 걸린다 — 아직 생성하지 않은 자리를 보면 학습 때는 정답을 베끼고 추론 때는 존재하지 않는 값을 참조하게 되기 때문이다.
정렬을 대신하는 자리
디코더에는 층마다 어텐션이 하나 더 있다. 크로스 어텐션은 질의를 디코더의 현재 상태에서, 키와 값을 인코더의 출력에서 가져온다. 「지금 이 타겟 토큰을 만들려는데 소스의 어느 자리를 봐야 하는가」를 묻는 장치다.
이것이 통계 번역의 정렬 모델이 하던 일을 그대로 대신한다. 다른 점이 둘이다. 첫째, 정렬이 학습 목표가 아니라 부산물이다. 정렬을 따로 학습시키지 않고 번역을 잘하도록만 학습시키는데, 그 과정에서 크로스 어텐션의 가중치가 저절로 대응 관계에 가까운 모양이 된다. 둘째, 대응이 하나로 고정되지 않는다. 통계 정렬은 「학교↔school」처럼 이산적인 대응표를 만들지만, 크로스 어텐션은 매 토큰마다 소스 전체에 걸친 가중치 분포를 내므로 여러 자리를 동시에 조금씩 볼 수 있다. 한국어의 「간다」를 영어로 옮기며 주어와 동사를 함께 봐야 하는 자리에서 이 성질이 값을 한다.
다만 크로스 어텐션 가중치를 정렬 그림으로 그대로 읽는 것은 조심해야 한다. 층과 헤드마다 보는 것이 달라 어떤 헤드는 대응을, 어떤 헤드는 위치나 문장 부호를 본다. 특정 층의 특정 헤드를 골라 봐야 사람이 아는 정렬에 가까워진다. 번역이 왜 그렇게 나왔는지를 설명해야 하는 자리, 이를테면 검수자에게 근거를 보여 주는 화면에서는 이 점을 알고 써야 한다. 어텐션 그림은 설명의 힌트이지 모델이 실제로 계산한 대응의 증명이 아니다.
서브워드 분할
공유 어휘와 분리 어휘
번역 모델의 어휘는 두 가지로 구성할 수 있다. 소스와 타겟이 어휘 사전을 함께 쓰는 공유 어휘, 각자 따로 갖는 분리 어휘다.
공유 어휘는 문자 체계가 겹치는 언어 쌍에서 유리하다. 고유명사와 숫자, 기술 용어가 양쪽에 같은 형태로 나오므로 한 번 배운 표현을 양방향에서 쓸 수 있고, 인코더와 디코더의 임베딩을 묶어 파라미터를 아낄 수도 있다. 한국어와 영어처럼 문자 체계가 완전히 다른 쌍에서는 이 이득이 작다 — 한글과 라틴 문자가 겹칠 일이 없어 사실상 어휘가 둘로 갈려 있는 것과 같고, 공유로 두면 한쪽 언어가 어휘 자리를 더 많이 차지하는 문제가 생긴다. 다국어 모델은 그럼에도 공유 어휘를 쓰는데, 언어가 수십 개일 때 따로 두는 것이 불가능하기 때문이다.
형태소 경계와 BPE
BPE 같은 서브워드 알고리즘은 말뭉치에서 자주 붙어 나오는 문자 쌍을 반복해서 병합한다. 빈도만 보므로 언어학적 경계를 모른다.
한국어에서 이것이 문제가 되는 자리가 있다. 「학교에서」의 형태소 경계는 「학교 + 에서」인데, BPE는 「학교에」와 「서」로 자를 수도 있다. 그러면 「학교에」라는 토큰이 「학교」와 다른 것이 되어, 「학교를」·「학교가」와의 공통점을 모델이 처음부터 다시 배워야 한다. 조사가 여럿이고 각각 흔하므로 같은 명사가 여러 개의 토큰 조각으로 흩어진다.
실무 대응은 둘이다. 하나는 사전학습 단계에서 형태소 분석을 먼저 돌려 그 경계 위에서 BPE를 학습시키는 것이고, 이것이 한국어를 따로 학습한 모델들이 쓰는 방식이다. 다른 하나는 어휘 크기를 키우는 것인데 효과가 제한적이다 — 어휘 크기와 한국어 분절 효율의 상관이 사실상 0이라는 실측이 한국어 토큰세에 있다. 같은 한국어 글이 토크나이저에 따라 토큰 수가 3배까지 벌어지고, 그 차이를 만드는 것은 사전의 크기가 아니라 한국어를 따로 학습했는가였다.
분절이 품질에 닿는 경로
분절이 나쁘면 두 가지가 함께 나빠진다. 같은 내용을 옮기는 데 토큰이 더 들어 입력 길이 한계에 일찍 닿고, 한 개념이 여러 조각으로 흩어져 표현이 흐려진다. 번역에서는 후자가 특히 고유명사에서 드러난다 — 이름이 여섯 조각으로 쪼개지면 모델이 그것을 하나의 개체로 다루기 어려워 음차가 흔들리거나 일부가 빠진다. 사람 이름과 제품명이 자주 나오는 문서를 번역할 때 이 자리를 먼저 본다.
NLLB-200
한 모델에 200개 언어
Meta가 공개한 NLLB-200은 200개 언어 사이를 한 모델로 번역한다. 언어 쌍으로 세면 4만 개에 가까운 방향을 하나의 파라미터 집합이 감당한다는 뜻이다.
가능한 이유는 언어들이 표현을 공유하기 때문이다. 어순이나 형태 구조가 닮은 언어들은 인코더 안에서 비슷한 표현으로 모이고, 한 언어에서 배운 것이 다른 언어로 넘어간다. 이 넘어감이 데이터가 적은 언어에서 특히 크다 — 병렬 말뭉치가 수만 문장뿐인 언어도, 닮은 언어의 데이터에 얹혀 쓸 만한 품질에 도달한다.
from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
model_name = "facebook/nllb-200-distilled-600M"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForSeq2SeqLM.from_pretrained(model_name)
def translate(text: str, src_lang: str, tgt_lang: str) -> str:
tokenizer.src_lang = src_lang
inputs = tokenizer(
text, return_tensors="pt",
max_length=512, truncation=True,
)
generated = model.generate(
**inputs,
# 타겟 언어 토큰을 디코더의 첫 토큰으로 강제한다
forced_bos_token_id=tokenizer.convert_tokens_to_ids(tgt_lang),
num_beams=5,
max_length=512,
no_repeat_ngram_size=3,
)
return tokenizer.decode(generated[0], skip_special_tokens=True)
print(translate("오늘 날씨가 매우 좋습니다.", "kor_Hang", "eng_Latn"))
샘플링 온도
다국어 모델을 학습할 때 언어별 데이터 양은 극단적으로 불균형하다. 영어-프랑스어 병렬 말뭉치는 수억 문장인데 어떤 언어 쌍은 수만 문장이다. 이 비율대로 배치를 뽑으면 작은 언어는 학습 내내 거의 안 나온다.
그래서 온도 샘플링을 쓴다. 각 언어 쌍이 뽑힐 확률을 원래 비율의 승에 비례하게 두는 방식이다. 이면 원래 비율 그대로이고, 를 키울수록 분포가 평평해져 작은 언어가 더 자주 뽑힌다.
이 값이 곧 트레이드오프의 손잡이다. 온도를 올리면 저자원 언어가 좋아지는 대신 고자원 언어가 원래 낼 수 있는 품질보다 떨어진다. 데이터가 많은 언어의 문장을 덜 보게 되기 때문이다. 다국어 모델을 파인튜닝해 특정 언어 쌍에 쓰려는 경우라면 이 균형이 이미 우리 쪽에 불리하게 잡혀 있다는 뜻이므로, 그 쌍의 데이터로 추가 학습하는 값이 크다.
언어별 편차
200개 언어를 하나로 묶어 평균 점수를 보면 실제 사용 판단이 안 된다. 같은 모델 안에서도 언어마다 품질이 크게 다르고, 그 차이는 그 언어의 학습 데이터 양과 닮은 언어의 존재 여부로 거의 설명된다.
그래서 도입 전에 반드시 우리가 쓸 언어 쌍만 따로 잰다. 그리고 방향도 따로 잰다 — 한국어에서 영어로 가는 품질과 영어에서 한국어로 오는 품질은 같은 모델에서도 다르다. 대체로 영어 쪽으로 가는 방향이 낫고, 영어에서 형태가 복잡한 언어로 가는 방향이 어렵다. 들어오는 문장을 영어로 옮겨 처리한 뒤 다시 돌려보내는 구성을 짤 때 이 비대칭이 곧바로 드러난다 — 두 방향의 품질이 다르므로 왕복하면 나쁜 쪽이 전체 품질을 정한다.
번역 평가 지표
BLEU와 토큰화
BLEU는 생성 번역과 참조 번역 사이의 n-gram 정밀도를 1부터 4까지 구해 기하평균 내고, 번역이 참조보다 짧으면 벌점을 곱한다. 짧게 내놓아 정밀도를 올리는 꼼수를 그 벌점이 막는다.
한국어에서 이 지표를 쓸 때 반드시 확인할 것이 토큰화다. BLEU는 토큰이 겹치는지를 세는데, 무엇을 토큰으로 볼지에 대한 규정이 지표 안에 없다. 기본 설정은 대체로 공백과 문장 부호를 기준으로 자르므로 한국어에서는 어절 단위가 되고, 그러면 「학교에」와 「학교를」이 다른 토큰이라 겹치지 않는다. 뜻이 같아도 조사가 다르면 0점이라는 뜻이고, 이 때문에 한국어 BLEU는 같은 품질의 영어 번역보다 항상 낮게 나온다.
대응은 형태소 단위로 토큰화한 뒤 재는 것이다. 중요한 것은 어느 설정으로 쟀는지를 함께 적는 일이다. 토큰화가 다르면 같은 번역에도 점수가 달라지므로, 설정을 안 밝힌 BLEU 수치는 다른 수치와 비교할 수 없다. 사내에서 쓰는 수치라면 계산 코드를 한 곳에 두고 모두가 그것만 부르게 하는 편이 규약을 문서로 적어 두는 것보다 확실하다. 지표가 달라 보이는 원인의 대부분은 모델이 아니라 재는 방식이 바뀐 것이다.
chrF와 COMET
chrF는 단어가 아니라 문자 n-gram의 F-점수를 잰다. 조사나 어미가 조금 달라도 앞쪽 문자들이 겹치므로 부분 점수를 받고, 그래서 형태가 복잡한 언어에서 BLEU보다 사람 판단에 가깝다. 토큰화 설정에 덜 흔들린다는 점도 실무에서 크다.
COMET은 접근 자체가 다르다. 겹침을 세는 대신, 사람이 매긴 번역 품질 점수를 학습한 신경망이 소스·번역·참조 셋을 함께 보고 점수를 예측한다. 표현이 달라도 뜻이 맞으면 높은 점수를 주고, 단어는 겹치는데 뜻이 어긋나면 낮은 점수를 준다. 대신 모델을 돌려야 하므로 느리고, 학습에 쓰인 언어와 도메인 밖에서는 신뢰도가 떨어진다.
실무 구성은 셋을 층으로 쓰는 것이다. 개발 중 빠른 회귀 확인은 chrF로, 외부에 보고하는 수치는 BLEU로(토큰화 설정을 명시해서), 최종 품질 판단은 COMET과 사람 평가로 한다.
한국어 번역에서 갈리는 자리
어순 역전
한국어는 주어-목적어-동사이고 영어는 주어-동사-목적어다. 짧은 문장에서는 단어 몇 개의 자리가 바뀌는 정도지만, 관계절이 붙으면 이야기가 달라진다. 영어의 관계절은 꾸미는 명사 뒤에 오고 한국어의 관형절은 앞에 온다 — 「the report that the team submitted last week」는 「팀이 지난주에 제출한 보고서」가 되어, 원문의 뒤쪽 덩어리가 통째로 앞으로 와야 한다.
이 재배치가 어텐션이 실제로 값을 하는 자리다. 순환 신경망 시절에는 소스를 순서대로 읽으며 출력을 내야 했으므로 멀리 떨어진 덩어리를 앞으로 당겨 오는 일이 구조적으로 어려웠다. 어텐션은 매 출력 토큰마다 소스 전체를 다시 보므로 순서에 매이지 않는다. 그래서 이 언어 쌍에서 신경망 번역으로 넘어올 때의 품질 향상이 유난히 컸다.
한자어도 방향에 따라 난이도가 갈린다. 한국어 어휘의 절반 이상이 한자에서 온 말이라 중국어·일본어와는 대응이 대체로 곧게 이어지는 반면, 영어로 옮길 때는 「고가도로」·「수리권」처럼 한 낱말이 영어에서는 구로 풀려야 하는 자리가 많다. 전문 용어가 몰려 있는 문서일수록 이 방향의 품질이 떨어지는 이유다.
문장 단위가 깨뜨리는 것
번역 시스템은 대개 문장 하나를 받아 문장 하나를 낸다. 구현이 단순하고 병렬 처리가 쉽기 때문인데, 이 단위가 한국어 번역에서 세 가지를 망가뜨린다.
첫째는 대명사다. 영어 원문의 「it」이 앞 문장의 무엇을 가리키는지 모르면 한국어로 옮길 때 생략할지 명사로 풀지를 정할 수 없다. 둘째는 경어 등급이다. 한국어는 문장마다 높임 여부를 골라야 하는데 그 근거가 문장 안에 없다. 문서의 성격과 화자·청자 관계가 정하는 것이라, 문장 단위로 독립해 번역하면 한 문서 안에서 「합니다」와 「한다」가 섞인다. 셋째는 용어 일관성이다. 같은 용어가 문단마다 다르게 옮겨진다 — 앞에서 「접근성」이었던 것이 뒤에서 「어프로치빌리티」가 된다.
문맥 창 주기
해법은 문장 앞에 앞 문맥을 함께 넣어 주는 것이다. 앞 문장 한두 개를 구분자와 함께 붙여 입력하고, 출력에서는 마지막 문장만 취한다. 이것만으로 대명사와 경어 일관성이 눈에 띄게 좋아진다.
비용이 붙는다. 입력이 길어져 추론이 느려지고, 같은 문장을 여러 번 인코딩하게 된다. 그리고 앞 문맥에 오역이 있으면 그것이 뒤로 전파된다. 앞 문맥을 몇 문장까지 줄지는 문서 성격이 정한다. 대화록처럼 화자가 바뀌는 문서는 앞 발화 두세 개가 필요하고, 조항이 독립된 계약서는 한 문장이면 충분하다. 문서 전체를 한 번에 넣을 수 있는 긴 컨텍스트 LLM이 이 문제에 강한 이유가 여기 있다 — 문서 안의 모든 문장이 서로를 볼 수 있으므로 일관성이 구조적으로 확보된다. 대신 문장 단위 병렬 처리를 포기해야 하고 비용이 크게 오른다.
용어 일관성과 모델 선택
용어집을 강제하는 세 가지 방법
회사 이름, 제품명, 법률 용어처럼 반드시 정해진 번역을 써야 하는 말이 있다. 강제하는 방법이 셋이고 대가가 각각 다르다.
후처리 치환은 번역이 끝난 문장에서 문자열을 갈아 끼운다. 구현이 가장 쉽고 확실하지만, 한국어에서는 조사가 앞 글자의 받침에 따라 달라지므로 치환 후 「제품를」 같은 것이 나온다. 조사 교정을 함께 붙여야 한다. 제약 디코딩은 생성 중에 지정한 토큰 열이 반드시 나오도록 빔 서치를 제한한다. 문법적으로 자연스러운 자리에 들어가지만 구현이 복잡하고 디코딩이 느려진다. 파인튜닝은 용어가 들어간 문장 쌍으로 모델을 추가 학습한다. 가장 자연스러운 결과를 내지만 용어가 바뀔 때마다 다시 학습해야 하므로, 자주 바뀌지 않는 핵심 용어에만 쓴다.
세 가지를 겹쳐 쓰는 것이 보통이다. 핵심 용어 수십 개는 파인튜닝으로, 나머지는 후처리로 덮고, 절대 틀리면 안 되는 몇 개만 제약 디코딩에 건다. 어느 방식을 쓰든 용어집 자체는 코드 밖의 데이터로 둔다 — 용어는 제품이 바뀔 때마다 늘고, 그때 고치는 자리가 번역 코드 안이면 배포가 한 번 더 필요해진다.
무엇을 고를 것인가
| 조건 | 고를 것 |
|---|---|
| 범용 번역, 구현이 빨라야 함 | 상용 번역 API |
| 오프라인·사내 실행 | NLLB-200 또는 언어 쌍 전용 공개 모델 |
| 저자원 언어 포함 | NLLB-200 계열 |
| 문맥·뉘앙스·용어 일관성이 중요 | 긴 컨텍스트 LLM |
| 대용량 배치, 지연이 중요 | 전용 모델 + 추론 최적화 런타임 |
LLM이 번역에서 전용 모델을 넘어서는 자리는 분명하다. 문서 전체를 보고 일관성을 잡는 일, 「이 문서는 기술 문서이니 경어를 쓰고 용어집을 따르라」 같은 지시를 받아들이는 일이 그렇다. 반대로 하루 수백만 문장을 낮은 지연으로 처리해야 한다면 전용 모델이 여전히 압도적으로 싸다. 판단 기준은 품질의 절대값이 아니라 문맥이 필요한 번역인가다.
번역은 소스 문장이라는 강한 제약 아래에서 문장을 만드는 일이었다. 다음 글에서는 그 제약을 걷어낸 자리, 곧 학습된 확률 분포에서 다음 토큰을 골라 글을 만들어 가는 디코딩 자체를 다룬다. 여기서 쓴 빔 서치와 no_repeat_ngram_size 가 거기서는 손잡이 하나하나로 열린다.
읽어주셔서 감사합니다. 😊

