N-gram 이해해보기
- 개발
- 2026. 5. 14. 00:12
TL;DR; N그램을 설명한 글입니다.
LLM from Scratch - Part 1. Statistical Language Models
[LLM 바닥부터 만들기 - 파트1. 통계적 언어 모델의 원리] 부담 없이 즐기며 배우는 LLM 입문 강의 🚀오픈이벤트 50%할인! (~5/17이후 가격이 인상됩니다.)
www.honglab.ai
N-gram의 아이디어는 1948년 Claude Shannon 의 정보이론 논문에서 시작되었습니다. 핵심은 단순합니다. "다음 글자를 이전 글자를 기반으로 예측할 수 있지 않을까?" 라는 아이디어였습니다. 이후 1980년대에 들어서며 컴퓨터 성능이 발전하였고, 이러한 통계 기반 언어 모델을 실제로 활용할 수 있는 환경이 만들어지기 시작하였습니다. 그렇게 N-gram은 음성 인식, 자동 완성, 기계 번역과 같은 다양한 자연어 처리 분야에서 활용되기 시작하였습니다. 현대의 LLM은 N-gram보다 훨씬 복잡한 구조를 사용하지만, "이전 문맥을 기반으로 다음 단어를 예측한다"는 핵심적인 방향성 자체는 크게 다르지 않습니다. 그렇다면 이런 의문이 들 수 있습니다. "이렇게 단순한 아이디어인데, 왜 실제로 이해하고 학습하는 것은 어려울까?" 겉으로 보면 단순히 다음 단어를 연결하는 방식처럼 보입니다. 하지만 N이 증가할수록 경우의 수가 급격하게 증가하게 되고, 데이터 부족 문제 역시 함께 발생하게 됩니다.
이번 글에서는 N-gram이 어떤 방식으로 동작하는지, 그리고 왜 단순해 보이는 구조가 생각보다 복잡한 문제를 가지게 되는지 확인해보겠습니다.
그래서 N그램이 뭔데?
N-gram은 이전 글자를 기반으로 다음 글자를 예측하는 방법입니다. 예를 들어 토큰이 가 → 나 → 다 이렇게 존재한다고 가정해봅시다. 그렇다면 가 다음에는 무엇이 올까요? 바로 나입니다. 그렇다면 나 다음에는 무엇이 올까요? 바로 다입니다.
즉, 핵심은 단순합니다. 이전에 등장한 패턴을 기반으로 다음 글자를 예측하는 것입니다. 하지만 여기서부터 헷갈리기 시작합니다.
"AI는 이런 단순한 연결만 보고 어떻게 학습한다는 거지?"
실제로 N-gram은 우리가 생각하는 것보다 훨씬 단순한 구조입니다. 문장을 이해한다기보다, 이전에 등장했던 패턴과 확률을 기반으로 다음 글자를 이어붙이는 방식에 가깝습니다.
지금 예시는 하나의 경우만 존재하기 때문에 단순해 보입니다. 하지만 경우의 수가 수십, 수백 개로 증가한다면 이야기는 달라집니다.
그렇다면 이런 의문이 들 수 있습니다. "모든 경우를 전부 조합해서 계산하는 것일까?"
결론부터 말하면 완전히 틀린 생각은 아닙니다. 실제로 N-gram은 이전에 등장했던 패턴들을 저장하고, 현재 상태에서 다음에 등장할 가능성이 높은 토큰을 찾아 이어붙이는 방식에 가깝습니다. 즉, 문장을 이해한다기보다 이전에 등장했던 연결 관계를 기반으로 다음 토큰을 예측하는 구조라고 볼 수 있습니다.
아니 잠깐만 이거 끝말잇기랑 유사한 느낌인데...
N-gram은 현재 상태에서 다음에 등장할 가능성이 높은 토큰을 찾아 이어붙이는 방식이라고 하였습니다. 그렇다면 우리가 익숙하게 알고 있는 '끝말잇기'와 비슷한 느낌이라고 볼 수도 있지 않을까요?

예를 들어 끝말잇기 역시 현재 단어를 기준으로 다음에 어떤 단어가 이어질 수 있는지를 생각하게 됩니다. 만약 다음에 등장할 가능성이 높은 단어들을 어느 정도 예측할 수 있다면, 게임을 훨씬 유리하게 풀어나갈 수 있을 것입니다.
N-gram 역시 이와 유사하게 이전에 등장했던 패턴들을 기반으로 다음 토큰을 예측하는 방식이라고 볼 수 있습니다.
이 개념을 조금 더 직관적으로 이해하기 위해, 저는 우리말샘 + 표준국어대사전 + 한국어기초사전에서 총 548,385개의 단어를 추출하였습니다. 여기에는 한 글자 단어, 속담, 관용구, 구 형태의 표현은 제외하였으며, 최근 등장한 일부 신조어 역시 포함되지 않았습니다.
그렇다면 이 단어들을 기반으로 N-gram이 어떤 방식으로 동작하는지 직접 확인해봅시다.
저희는 현재 끝말잇기를 기반으로 N-gram을 이해하려고 하기 때문에, 먼저 가 ~ 힣까지 어떤 글자들이 얼마나 등장하는지 빈도수를 확인해보겠습니다.
1 - gram(Unigram)

빈도수를 확인해보니, 다가 포함된 단어는 전체의 약 5%를 차지하고 있었습니다.

반면 힗, 힡과 같은 글자는 단 1회만 등장하였으며, 비율로 보면 약 0.0001% 수준에 불과하였습니다.
일단 이 방식이 얼마나 효과적인지 확인하기 위해, 무작위 문자열을 생성해 보겠습니다.

단어를 무작위로 조합해 보았지만, 사전 기준으로는 의미를 가지지 않는 문자열이 대부분이었습니다.
즉, 단순히 글자를 이어 붙인다고 해서 자연스러운 단어가 만들어지는 것은 아니었습니다. 그렇다면 이런 의문이 들 수 있습니다.
"의미는 없더라도, 실제 한국어 단어에서 자주 나타나는 글자 배열을 따라가면 더 자연스러운 문자열을 만들 수 있지 않을까?"
이제부터는 실제 단어에서 나타나는 글자 패턴과 빈도수를 바탕으로, 무작위 문자열이 얼마나 단어처럼 보이도록 생성될 수 있는지 확인해 보겠습니다. 이거에 대한 아이디어가 바로 N-gram입니다.
2-gram (bi-gram)
그렇다면 같은 방식으로 2-gram(Bigram)으로 만들면 어떻게 변할까요?
이제부터는 단순히 글자의 등장 횟수만 보는 것이 아니라, 현재 글자 다음에 어떤 글자가 자주 등장하는지 함께 확인하게 됩니다. 즉, 이전 글자 1개를 기반으로 다음 글자를 예측하는 방식으로 변경됩니다. 결과는 다음처럼 나왔습니다.

'다'를 기준으로 다시 비교해보면, 기존에는 약 5%의 비율을 차지하고 있었지만 2-gram(Bigram)에서는 약 3% 수준으로 감소한 것을 확인할 수 있었습니다. 왜 이런 차이가 발생한 것일까요?
실제로 생성된 문자열을 확인해보니 EOS, BOS와 같은 특수 문자가 함께 포함되어 있는 것을 발견할 수 있었습니다.
- BOS(Beginning Of Sequence) : 문자열의 시작을 의미
- 예: _다, _라, _가
- EOS(End Of Sequence) : 문자열의 끝을 의미
- 예: 다_, 라_, 가_
즉, 2-gram부터는 단순히 글자 자체만 보는 것이 아니라, 앞뒤 위치 정보까지 함께 고려하게 됩니다. 따라서 기존에는 단순 포함 기준으로 계산되던 다의 비율이, 실제 문맥 기준으로 다시 계산되면서 비율 차이가 발생하게 된 것입니다.
그렇다면, 2-gram을 이용해서 무작위 문자열을 만들어보겠습니다.

1-gram과의 가장 큰 차이점은 시작 지점과 종료 지점이 존재한다는 점입니다. 즉, <BOS>에서 시작하여 <EOS>로 종료되는 구조를 가지게 됩니다. 아직은 왜 이런 차이가 발생하는지 직관적으로 이해되지 않을 수 있습니다. 하지만 실제로 생성된 문자열을 확인해보면, 1-gram에 비해 훨씬 실제 단어와 비슷한 형태가 등장하는 것을 확인할 수 있었습니다.
여기서 한 가지 의문이 들었습니다. "왜 1-gram은 최대 길이를 6자리로 제한했을까?"
그 이유는 너무 짧게 생성할 경우, 1-gram 역시 우연히 실제 사전에 존재하는 단어가 등장할 가능성이 높아지기 때문입니다. 즉, 모델이 자연스럽게 생성한 것인지, 단순히 짧아서 우연히 맞아떨어진 것인지 구분하기 어려워집니다.
따라서 조금 더 명확하게 차이를 확인하기 위해 1-gram은 최대 6자리로 고정하여 비교를 진행하였습니다. 참고로 2-gram 또한 max는 6자리입니다.
실제로 생성된 결과를 사전과 비교해보니, 100개의 문자열 중 27개가 실제 사전에 등재된 단어인 것을 확인할 수 있었습니다. 단순히 글자의 등장 빈도만 기반으로 생성하였음에도 생각보다 실제 단어가 꽤 많이 등장한 것입니다.
즉, 완전히 의미를 이해하지 못하더라도 자주 등장하는 글자 패턴만으로도 어느 정도 자연스러운 문자열이 만들어질 수 있다는 사실을 확인할 수 있었습니다.
정확도를 높이기 위해 gram의 차원을 높인다?
여기서 알 수 있었던 점은, gram의 차원이 높아질수록 생성 결과의 정확도 역시 함께 높아진다는 사실이었습니다. 실제로 N 값을 증가시키면 이전 문맥을 더 많이 참고하게 되기 때문에, 생성되는 문자열 역시 훨씬 자연스러워지는 경향이 있습니다.
하지만 무조건 높다고 좋은 것은 아니었습니다. 차원이 지나치게 증가하게 되면 학습 데이터에 존재하지 않는 문맥에서는 제대로 동작하지 못하게 됩니다. 또한 새로운 패턴을 생성하기보다는, 학습했던 문자열 자체를 그대로 기억하는 방향에 가까워질 수 있습니다. 즉, 문맥을 일반화해서 이해한다기보다 단순히 긴 패턴을 외워버리는 형태에 가까워지는 것입니다.
왜 이런 현상이 발생할까요?
N이 증가할수록 경우의 수 역시 함께 폭발적으로 증가하기 때문입니다.
예를 들어 다음과 같이 문맥을 기억한다고 가정해봅시다.
- 2-gram: 하 → 다
- 3-gram: 사랑 → 하
- 4-gram: 사랑하 → 다
- 5-gram: 사랑하다 → <EOS>
이처럼 N이 증가할수록 더 긴 문맥과 구체적인 패턴을 함께 참고하게 됩니다. 문제는 문맥이 지나치게 구체적이 되면, 학습 데이터에서 단 한 번도 등장하지 않은 조합들이 급격하게 많아진다는 점입니다.
이를 Sparsity(희소성 문제)라고 합니다.
예를 들어 다음과 같은 문맥이 존재한다고 가정해봅시다.
므쀍졀
이런 조합은 실제 학습 데이터에 존재하지 않을 가능성이 매우 높습니다. 그렇다면 모델은 다음 글자를 예측할 수 없게 됩니다.
결국 N을 증가시키면 다음과 같은 현상이 동시에 발생하게 됩니다.
- 문맥 정확도 증가
- 하지만 데이터 커버리지 감소
즉, 더 자연스러운 결과를 만들 수는 있지만, 반대로 학습 데이터에 없는 새로운 문맥에는 점점 더 약해지게 되는 것입니다. 결국 문맥을 일반화하여 예측하는 모델이라기보다, 학습 데이터를 기억하고 재현하는 방향에 가까워지게 됩니다. 그렇다면, 2-gram까지 진행해봤는데 3,4,5,6까지 진행하면서 한번 몸소 느껴봅시다.
3-gram 부터는 빠르게 진행이 되어집니다.
3-gram


확실히 단어의 길이가 줄었다는 것을 알 수 있습니다. 확률도 100개중에 71개가 실제로 사전에 존재하는 단어입니다.
4-gram


5-gram


99/100
마지막 6-gram


놀라운 점은 5-gram과 6-gram의 확률 결과가 거의 동일하게 나타났다는 사실이었습니다. 즉, 문맥을 하나 더 늘렸음에도 생성 결과에는 큰 차이가 발생하지 않았습니다.
또한 이번에는 문자열 길이를 6자리로 제한하였기 때문에, 생성된 100개의 결과 중 무려 98개가 실제 사전에 등재된 단어인 것을 확인할 수 있었습니다.
이는 N이 증가할수록 모델이 새로운 패턴을 생성하기보다, 학습 데이터에 존재했던 문자열 자체를 그대로 재현하는 방향에 가까워지고 있다는 점을 보여주는 결과라고 볼 수 있습니다.
N-gram의 차수를 증가시킬수록 저장해야 하는 글자 조합의 수 역시 함께 증가하였습니다. 실제로 2-gram counts 파일은 약 5.6MB 수준이었지만, 6-gram counts 파일은 약 66.4MB까지 증가한 것을 확인할 수 있었습니다.
즉, 더 긴 문맥을 참고할수록 생성 결과는 더욱 자연스러워질 수 있었지만, 반대로 저장해야 하는 패턴 수 역시 급격하게 증가하게 됩니다. 결국 문맥 정확도를 높이기 위해서는 그만큼 더 많은 저장 공간과 계산 비용이 필요하게 되는 것입니다.
마무리
LLM의 예측 방식과 연결되는 개념 중 하나인 N-gram에 대해 알아보았습니다. 핵심은 이전 문맥을 기반으로 다음에 어떤 문자가 등장할지를 예측하는 구조였습니다. 실제로 N의 차원이 증가할수록 생성 결과는 더욱 자연스러워졌습니다. 하지만 반대로 문맥이 지나치게 길어질 경우, 새로운 패턴을 예측하는 것인지, 아니면 학습 데이터를 그대로 외워서 재현하는 것인지 구분하기 어려워지는 문제 역시 함께 확인할 수 있었습니다. 또한 차원이 증가할수록 경우의 수와 저장해야 하는 패턴 역시 함께 폭발적으로 증가하였고, 결국 희소성 문제(Sparsity) 역시 발생하게 되었습니다. 물론 이번 실험은 어디까지나 N-gram의 동작 원리를 이해하기 위한 단순화된 방식에 가깝습니다. 실제 LLM은 단순히 문자열 패턴만 저장하는 것이 아니라, 훨씬 복잡한 구조를 기반으로 문맥과 의미를 함께 처리합니다. 다음에는 실제 문서를 학습시켜 새로운 문장을 생성하는 방식에 대해 조금 더 깊게 확인해보겠습니다.
'개발' 카테고리의 다른 글
| 그레들DSL은 어떤것을 사용해야 할까? (0) | 2026.05.10 |
|---|---|
| 배치 작업시 예외가 발생한다면 어떻게 처리할까? (0) | 2026.05.06 |
| java에서 println()을 사용하면 안되는 이유 (0) | 2026.05.01 |
| 카프카의 설정은 진짜일까?? - offset(3) (0) | 2026.04.26 |
| 카프카의 설정은 진짜일까?? - offset(2) (0) | 2026.04.22 |