라마 4는 한국어에 가장 친화적인 오픈소스 모델입니다
Llama 4 모델이 출시되지마자, 저희 Sionic AI는 토크나이저 vocab 구성을 분석하였습니다.
llama4의 토크나이저 구성이 한국어 표현 관점에서 기존 Llama3.3 대비 2.5배, 그리고 지금까지 한국어 지원 비율이 가장 높던 Qwen 대비해서도 크게 개선되었음을 알 수 있습니다.
Repository - Sionic Llama4 Token Editor
사이오닉 AI에서 이번에 공개하는 Llama4 Token Editor는 토크나이저를 분석하고, 특정 범주의 토큰 가중치를 조정할 수 있는 도구입니다. 이 도구는 주로 Llama, Qwen 계열 모델의 토큰화 방식을 분석하고, 한글 및 영어 토큰을 분석하고 일부 가중치를 조정하는 데 사용됩니다.
git clone https://github.com/sionic-ai/Llama4-Token-Editor
cd Llama4-Token-Editor
pip install -r requirements.txt
JSON
복사
토크나이저로 한글 토큰을 분석해보자
최근 LLM은 기존의 가속기용 언어외에도 기본적인 C, numpy 처럼 기초적인 언어로 구현되고 있습니다. 이는 학습과 이해 목적은 물론 성능의 최적화 등 여러 가지 이유로 큰 의미가 있습니다.
하지만 그중 상당 부분은 축약적으로 구현되어 있어서 다국어 지원이 안되는 경우가 있습니다. 이런 부분을 좀더 원리를 이해하고 구현한다면 다양한 디바이스에서의 (NPU, GPU, FPGA) 구현과 저수준, 고성능이 필요한 토큰 생성 전략에 직접적으로 큰 도움이 될 수 있습니다.
사이오닉은 다음 아티클을 통해 순수 CUDA 및 C 문법을 따르는 구현으로 최소한의 구현으로 llama 모델을 추론하고 그 과정에서 LLM 의 다국어 멀티바이트 언어를 다루는 방법을 설명한 바 있습니다.
우리가 언어 모델이 어떤 토큰을 가지고 있고, 그 토큰이 어떤 언어 범주에 속하는지 알 수 있다면, 특정 토큰을 더 자주 또는 덜 자주 사용하도록 유도할 수 있도록 할 수 있습니다. 이러한 동작을 가능하게 하려면 우리는 모델이 거대한 어휘 사전을 전수적으로 조사하여 각 토큰을 어떻게 분류할 지 정해야 합니다.
예를 들어 토큰이 순수 영어 알파벳 인가, 한글 표현을 포함할 수 있는가 또는 완성형 한글 음절이 잘 들어가 있는가 아니면 특수 문자로만 구성되어 있는가 와 같은 내용으로 구분할 수 있어야 합니다. 이런 분류 작업을 진행하는 과정에서 어느 범주에도 속하지 않는 토큰 이 발생할 수 있습니다.
토크나이저는 자연어를 일정한 규칙에 따라 잘게 분할하여 ID 형태로 변환합니다.
흔히 BPE(Byte Pair Encoding)나 SentencePiece 같은 방식이 사용되는데, 대형 언어 모델에는 수십만 개에 달하는 토큰이 마련되어 있습니다. 예를 들어 "apple"이라는 단어가 하나의 토큰이 될 수도 있고, "han"이라는 부분 조각(token)이 여러 개 합쳐져 한글 음절을 만들어낼 수도 있습니다.
결국 모델 내부에는 "문자열 -> 숫자 ID"로 대응 짓는 어휘 사전이 존재하며, 모델은 텍스트를 입력받을 때 이를 전부 숫자 ID 시퀀스로 바꿔서 이해하게 됩니다.
이번 Sionic Llama4 Token Editor 에서 먼저 주안점을 두고 있는 것은 "한국어" 입니다. 그 이후에 일본어와 다른 아시아권 계열의 언어들로 확장할 예정입니다. 언어 모델이 영어 중심으로 학습되어 있을 때, 한글 표현이 얼마나 지원되는지 확인하려면 "한글 토큰" 관련 통계를 보는 것이 빠르고 확실합니다. UTF-8 인코딩의 세부 규칙을 통해 "완성형 한글 음절"이나 "한글 범위를 포함하는 바이트 시퀀스"를 직접 찾아내는 것이지요.
또한 "모델에 있는 모든 토큰 ID"를 하나씩 디코딩(decode)하여 그 토큰이 어떤 문자열을 나타내는지 확인합니다. 그리고 그 문자열을 다시 UTF-8 바이트로 살펴보면서, "이 바이트들은 한글 범위를 표현할 수 있는가?", "전부 영문 알파벳인가?", "특수기호만으로 이뤄졌는가?" 같은 체크를 수행합니다.
3바이트짜리 UTF-8 시퀀스가 실제로 한글 음절(U+AC00 ~ U+D7A3)을 표현하는지를 엄격하게 검사해 "완성형 한글"임을 식별하기도 하고, 부분적으로 0xEA~0xED 범위에 속하는 바이트가 들어 있어 "한글 표현이 가능한 조각"인지 확인하기도 합니다. 여기서 한글 표현 가능성을 가진 토큰은 부분 BPE의 표현중 한국어를 표현할 가능성을 가진 바이트 부분 순서열을 다국어와 공유하는 토큰입니다.
한글 토큰 분석 원리
UTF-8 인코딩과 BPE 알고리즘을 역공학으로 분석하여 한글 문자를 식별합니다.
•
can_be_hangul_utf8() 바이트 시퀀스가 한글의 일부가 될 수 있는지 확인
•
is_complete_hangul_utf8() 3바이트 시퀀스가 완성형 한글인지 확인
한글 UTF-8 표현은 아래와 같아요.
•
한글 유니코드 범위: U+AC00 ~ U+D7A3 (가 ~ 힣)
•
UTF-8 인코딩에서 첫 바이트: 0xEA ~ 0xED
•
이후 바이트: 0x80 ~ 0xBF
LLM 크기가 수십 혹은 수백 기가바이트에 달하는 시점에서, "내가 사용하려는 모델이 과연 어느 정도 범위의 언어 토큰을 포함하고 있는지"를 직접 확인하기가 매우 어렵습니다.
특히 새로운 버전의 모델이 공개되거나, 한국어와 같은 특정 언어에 대한 지원이 얼마나 잘 이루어졌는지 궁금할 때, 전체 어휘 사전을 일일이 살펴보는 건 사실상 불가능에 가깝습니다.
따라서 이러한 문제를 풀기 위해 "vocabulary에 존재하는 모든 토큰"을 검사해주고, 한글이나 영문, 특수문자, 그 외 범주를 나누어주는 역할을 맡습니다. 모델에 따라서는 한글 표현이 아주 적을 수도 있고, 반대로 꽤나 많을 수도 있는데, 어떤 경우든 이 도구를 돌려 보면 각각의 분포가 수치로 바로 나타납니다.
토큰 ID를 알게 되면 어떤 의미가 있을까요?
카테고리별 토큰 ID 목록을 가지고 어떠한 일을 할 수 있을까요? "한글 관련 토큰"의 로그 확률을 조금 개선하여서, 모델이 한국어 문장을 더 잘 쓰게 유도할 수 있을 것입니다.
예를 들어 "한글 가능성 토큰"이나 "완성형 한글 토큰"만 모아 둔 목록에 대해, 모델이 추론할 때마다 점수를 +α만큼 더해주면, 생성된 문장 내에서 한글 표현이 선택될 확률이 올라갑니다.
반대로, 기호나 이모지 같은 토큰이 너무 자주 등장해서 결과물 품질이 떨어진다고 생각한다면, "특수문자 토큰" 목록에 -β만큼 편향을 줘서 기호 사용량을 줄이는 식으로도 응용할 수 있습니다. 인퍼런스 단계에서의 Fine Tuning은 대규모 모델 학습 없이도 "디코딩 시점의 로그 확률"만 바꾸어주므로, 짧은 시간 안에 큰 효과를 볼 수 있습니다.
그러나 이 과정을 직접 구현하려면 적잖은 수고가 듭니다. 우선 "모델에서 모든 토큰"을 가져와야 하고, 각 토큰을 다시 "문자열로 디코딩"해야 하며, "한글 범위를 판별"하기 위한 함수를 짜야 하고, "토큰 ID를 카테고리별로 분류"한 뒤 "텍스트 파일에 저장"하는 기능까지 일일이 만들어야 합니다.
이번 Sionic Llama4 Token Editor 는 이러한 일련의 과정을 하나의 스크립트로 간편화 하였습니다. 명령줄에서 python token_analyzer.py --model_id "모델명" 같은 방식으로 실행하기만 하면, 결과로 JSON 파일과 두 개의 텍스트 파일이 생성됩니다.
python token_analyzer.py --model_id "모델_경로_또는_이름"
# --output_file: 분석 결과를 저장할 JSON 파일 (기본값: token_category_analysis.json)
# token_category_analysis.json: 전체 분석 결과가 담긴 JSON 파일
# categorized_token_ids.txt: 분류된 토큰 ID 목록 (가중치 상향 조정용)
# uncategorized_token_ids.txt: 미분류 토큰 ID 목록 (가중치 하향 조정용)
Python
복사
분석 로직 살펴보기
이번 Sionic Llama4 Token Editor 코드에 대해 하나씩 살펴보고 어떤 의미가 있는지 살펴보겠습니다.
LLM에서는 흔히 BPE(Byte Pair Encoding) 방식을 통해 단어를 여러 조각으로 쪼개어 토큰화합니다. 예컨대 영어 단어 “apple”은 하나의 토큰이 될 수도 있지만, “app” + “le”로 나뉠 수도 있습니다.
102 번째 이하 토큰은 분석하지 않았습니다. 그 이유는 102 번째 이하 토큰 ID인 특수토큰이 모델마다 다르고 특수 토큰 확률을 건들면 모델이 크게 고장날수 있기 때문입니다.
한글은 3바이트 UTF-8 인코딩으로 표현되는 음절(가~힣)을 조합해야 하므로, 다양한 경우가 발생합니다. 예를 들어보겠습니다.
•
완성형 한글 토큰: “가”, “나”, “▁이순신”처럼 온전한 음절(또는 어절)을 담고 있는 토큰
•
부분 한글 토큰(Partial): “ㄱ” “ㅏ”와 같은 자모 조각이 합쳐진 형태이거나, “0xEA 0xB0” 등의 멀티바이트 중간 조각. 이 경우 정확히 한글 음절을 표현하지 못해도, 추후 다른 조각과 결합해 한글을 만들 수 있음
•
영문, 특수문자 등: 전혀 한글과 무관한 토큰도 존재
can_be_hangul_utf8(byte_or_sequence)
•
UTF-8 바이트 시퀀스가 한글 범위에 속할 가능성이 있는지 확인합니다. 0xEA~0xED 범위의 첫 바이트를 갖고, 이어지는 바이트가 0x80~0xBF 범위 안에 들어오는지 등 여러 조건을 체크합니다.
•
한글 완성형 문자는 보통 3바이트로 이루어지지만, BPE 토큰화 과정에서 일부가 잘려 나가서 부분적인 바이트 시퀀스가 다른 토큰과 합쳐질 수 있으므로, 전체적으로 한글을 구성할 잠재력이 있는 토큰을 빠짐없이 잡아내기 위해서 사용합니다.
is_complete_hangul_utf8(byte_sequence)
•
정확히 3바이트가 들어왔을 때, 그것이 완성형 한글 음절(가~힣)인지 판별한다. 첫 바이트가 0xEA~0xED 범위에, 그 뒤 바이트가 0x80~0xBF 범위에 속해야 하며, 0xEA로 시작하면 두 번째 바이트는 최소 0xB0 이상, 0xED로 시작하면 두 번째 바이트는 0x9F 이하인 것 등 세부 조건을 확인합니다.
•
이렇게 하면 완성형 한글 토큰이 포함된 경우에 해당 토큰 ID를 별도 집합(complete_hangul_ids)으로 분류할 수 있게 됩니다.
JSON 파일에는 전체 통계를 담게 됩니다. 예를 들면 "모델에 존재하는 토큰 수 중 순수 영문 토큰은 몇 %, 한글 완성형 음절을 포함하는 토큰은 몇 %, 특수문자 토큰은 몇 %, 미분류는 몇 %" 등등과 같은 내용이 담겨있는 것이지요. 최종 텍스트 파일에는 "분류된 토큰 ID 목록(categorized_token_ids.txt)"과 "미분류 토큰 ID 목록(uncategorized_token_ids.txt)" 이 각각 기록이 됩니다.
한 가지 생각해봐야 하는 사항은 "미분류 토큰"이 생각보다 많을 수 있다는 사실입니다. 왜냐하면 BPE나 SentencePiece 토큰화 방식에서는, 한글/영문/특수문자 외에도 숫자나 다른 언어, 그 밖에 섞인 문자열 같은 다양한 조합의 조각이 존재하기 때문입니다.
예컨대 숫자 '1234'만 모여 있으면 한글도, 영문도, 특수문자도 아니고 그냥 숫자이므로 미분류가 될 수 있습니다. 또는 영어와 숫자가 섞인 "abc123" 같은 조각 역시 별도의 정교한 분류를 하지 않으면 자연스럽게 미분류에 속하게 됩니다. 따라서 어떤 모델은 미분류 토큰이 매우 적고, 어떤 모델은 절반 가까이가 미분류로 잡힐 수도 있는데, 그 결과를 통해 "이 모델이 어떠한 문자/언어 영역을 중점적으로 다루고 있구나"라고 생각해볼 수 있겠습니다.
모델마다 한글 토큰이 차지하는 비율이 천차만별이므로, 이러한 결과치를 통해 "이 모델을 한국어 용도로 써도 괜찮을까?"를 어느 정도 가늠해 볼 수 있습니다. 혹은 한글 지원이 충분치 않다면, "한글 토큰에 로그확률 편향을 주는 방식"으로 보정할 수도 있겠지요. 이를 위해 transformers 라이브러리에서 제공하는 LogitsProcessor를 확장해 "특정 토큰 ID가 들어올 때마다 +1.0" 같은 식으로 점수를 조정하는 예시 코드도 흔히 사용됩니다.
완성된 한글 토큰만 따로 가중치를 높이면 안됩니다
완성된 한글 토큰만 따로 가중치를 높이는 것만으로는 충분치 않을 때가 많습니다. 왜냐하면 실제 텍스트 생성 과정에서 한글 한 음절을 이루는 모든 바이트가 토큰 분할된 뒤 서로 다른 형태로 분리될 수 있기 때문입니다.
만약 한글의 일부만 포함하는 부분적인 BPE 형태의 토큰에 가중치를 부여하지 않으면, 생성 과정에서 한글을 완성하기 위한 나머지 바이트가 쉽게 선택되지 않아 엉뚱한 출력 환각이 발생할 수 있게 됩니다.
“부분 바이트 시퀀스(Partial Byte Sequence)”
한글 문자가 정확히 세 바이트로 잘려 분리되지 않고, BPE 규칙상 절반 혹은 2~3바이트가 엇갈린 상태로 여러 조각 토큰에 들어가기도 합니다.
이 때, 토큰이 “한글 가능성”을 가질 수 있다는 사실(hangul_possible)을 놓치면, 나중에 디코딩 단계에서 필요한 토큰이 하나라도 누락되어 모델이 “환각(hallucination)” 상태에 빠지거나, 잘못된 문자 조합을 출력할 가능성이 높아집니다.
따라서 한글 문자를 부분적으로라도 구성할 수 있는 바이트를 포함한 모든 토큰에 대해서는 주의 깊게 분석하고, 가중치 조정 시에도 함께 다루어야 합니다.
어느 한 토큰에서 0xEA 0xB0 … 조합이 나오다가, 다음 토큰에서 0x80 … 등이 이어져야 완성형 “가”가 형성되는 식이죠. BPE 등으로 인해 한글 음절이 여러 조각인 바이트 수열로 나뉘는 경우가 흔합니다. 예를 들어 0xEA 0xB0 0x80이 모여야 “가”가 되는데, 토큰이 0xEA 0xB0까지만 담고, 나머지 0x80이 다른 토큰에 들어 있을 가능성도 존재합니다.
이런 식으로 나뉜 “부분” 토큰만 부스팅하고, “완성형 한글 토큰”은 낮추거나 그대로 두면, 모델이 결과적으로는 자모가 어색하게 끼인 문자열을 생성할 가능성이 생깁니다.
반대로 “완성형 한글” 토큰에만 점수를 주고, 부분 토큰은 전혀 가중치 조정을 하지 않으면, 모델이 어떤 문맥에서 부분 조각이 필요할 때 제대로 선택을 못 할 수도 있습니다. 예를 들면 일부 특수 케이스에서 자모가 합쳐져야 “분리된 외래어+한글 표기”가 가능한 상황이기 때문입니다.
만약 “온전한 한글”만 올리고, “부분 한글” 토큰들을 놓쳐버리면 오히려 모델이 엉뚱한 토큰을 선택해 “환각(hallucination)” 형태의 비정상 출력이 발생할 가능성이 큽니다. 따라서 부분 한글도 함께 편향을 주어야 모델이 부드럽게 한글 문장을 형성할 수 있습니다.
Llama 4의 긴 맥락을 활용하는 방법
Llama4 계열 모델이 자체적으로 "길어진 어휘 목록"과 "길어진 문맥"을 동시에 다루기 위해 Scalable-Softmax(SSMax), NoPE(No Positional Encoding) 등의 기법을 적용하고 있다는 점도 이번 글의 주제와 무관하지 않습니다. 기본 Softmax를 사용할 때 시퀀스 길이(n)가 지나치게 커지면 분모가 매우 커지면서 "attention fading" 현상이 발생합니다. 즉, 분포가 너무 평탄해져서 모델이 중요한 토큰에 집중하지 못하게 됩니다.
물론, Llama 4에서는 이를 보완하기 위해 SSMax 같은 방법을 도입해 긴 문맥에서도 분포가 날아가지 않도록 개선했다고 합니다. 다시 말해서 모델이 한글과 같은 멀티바이트 토큰을 많이 다루거나, 혹은 다국어로 된 긴 텍스트를 처리할 때도 중요한 곳에 집중할 수 있게 설계했다는 뜻이 됩니다.
이렇게 모델 내부가 개선되어 있더라도, 실제 어휘 사전이 한글 조각들을 얼마나 충실히 반영했는지, 그리고 얼마나 많은 수의 한글 음절을 완전하게 커버하는지 등은 여전히 토큰 레벨에서 분석해 봐야만 알 수 있습니다.
결국 Llama4 Token Editor를 한 번 돌려보면, "내가 쓰는 모델 안에는 어떤 형태의 토큰들이 어느 정도 비중으로 존재하는가"가 명확해집니다. 특히 한글, 영문, 특수문자, 숫자, 그 외 조합에 대한 전수 조사가 이루어지므로, 모델 상태를 진단하기에 적절합니다. 분석이 끝난 뒤 생성되는 카테고리별 텍스트 파일을 다시 디코딩해 보면, 흥미로운 토큰이 잔뜩 보일 수도 있습니다.
예를 들어 "▁이순신" 같은 특정 단어가 통째로 들어가 있다든지, 숫자와 문자가 엉켜 있는 복합 조각들이 많이 존재한다는 사실을 알 수도 있습니다. 이 모두가 BPE로 잘게 쪼개진 결과물이기 때문에, 모델 내부에서 어떤 식으로 텍스트가 해석되고 있는지 한눈에 파악할 수 있습니다.
만약 이 분석 결과를 바탕으로 모델 생성이 마음에 들지 않는다면, 로그의 확률을 조정하는 작업을 시도해볼 수도 있겠습니다. 예컨대 categorized_token_ids.txt 내부에 한글이 들어올 수 있는 가능성이 있는 토큰 ID가 대거 포함되어 있다면, 이를 불러와서 +1.2 정도의 점수를 더해주는 식으로 LogitsProcessor를 설정해 볼 수도 있겠죠?
그렇다면 이전과 동일한 프롬프트로 텍스트 생성을 돌려 보면, 한글 출력이 훨씬 자주 등장할 수 있습니다. 반대로 특수문자만 나열된 토큰 리스트에 -0.8 정도 페널티를 주면, 이상한 기호가 자주 튀어나오는 문제를 완화할 수도 있을 것입니다.
vLLM 인퍼런스 시의 코드 예시
아래 코드는 vLLM에서 오픈소스 OpenAI API 호환 서버를 띄운 뒤, request.logit_bias를 설정해 특정 토큰을 강제적으로 점수 높이거나 낮추는 예시입니다. categorized_token_ids.txt 파일이 굳이 억제해야 할 그리고 억제하지 않아야 할 토큰 목록 파일인 uncategorized_token_ids.txt 파일을 활용해서 vLLM의 OpenAI API에서도 로그 확률 편향을 줄 수 있습니다.
# vllm/entrypoints/openai/api_server.py 예시
# https://github.com/vllm-project/vllm/blob/95d63f38c039e6fce57cf9cddb4c32bbc655a376/vllm/entrypoints/openai/api_server.py#L465-L485
@router.post("/v1/chat/completions")
async def create_chat_completion(request: ChatCompletionRequest,
raw_request: Request):
handler = chat(raw_request)
if handler is None:
return ...
# 한글 토큰을 미리 수집한 목록, 예: [100145, 105714, 115668, ...]
logit_bias_list = [100145, 105714, 115668, 104949, 104437]
logit_bias = {str(token): 20 for token in logit_bias_list}
request.logit_bias = logit_bias
generator = await handler.create_chat_completion(request, raw_request)
# 이후 generator로부터 streaming 또는 JSON response 반환
...
Python
복사
HuggingFace Transformers에서 특정 토큰에 편향을 추가하는 프로세서 예시는 다음과 같습니다. LogitsProcessor 를 확장하여 사용하실 수도 있습니다.
from transformers import AutoModelForCausalLM, AutoTokenizer, LogitsProcessorList
import torch
# 특정 토큰에 바이어스를 추가하는 프로세서 구현
class TokenBiasLogitsProcessor:
def __init__(self, token_ids, bias_value):
self.token_ids = token_ids
self.bias_value = bias_value
def __call__(self, input_ids, scores):
# 특정 토큰 ID에 바이어스 적용
for token_id in self.token_ids:
scores[:, token_id] += self.bias_value
return scores
# 모델과 토크나이저 로드
model = AutoModelForCausalLM.from_pretrained("your_model_path")
tokenizer = AutoTokenizer.from_pretrained("your_model_path")
# categorized_token_ids.txt에서 토큰 ID 로드
with open("categorized_token_ids.txt", "r") as f:
content = f.read()
# "token_bias = [103,104,...]" 형식에서 ID 목록만 추출
ids_str = content.replace("token_bias = [", "").replace("]", "")
categorized_ids = [int(id_str) for id_str in ids_str.split(",")]
# 바이어스 값 설정
token_bias = 1.2 # 가중치 상향 조정값
# 입력 프롬프트
prompt = "여기에 프롬프트를 입력하세요"
input_ids = tokenizer(prompt, return_tensors="pt").input_ids
# 토큰 바이어스 프로세서 생성
token_bias_processor = TokenBiasLogitsProcessor(categorized_ids, token_bias)
logits_processor = LogitsProcessorList([token_bias_processor])
# 생성 실행
output = model.generate(
input_ids,
logits_processor=logits_processor,
max_length=100,
do_sample=True,
temperature=0.7
)
# 결과 출력
generated_text = tokenizer.decode(output[0], skip_special_tokens=True)
print(generated_text)
Python
복사