https://choiwonjin.tistory.com/62
[10/23] 아이펠 리서치 15기 TIL | [NLP] 토큰화 (Tokenization): 형태소 분석, 품사 태깅
어제까지 공부했던 CV 파트를 마무리하고, 이제부터 NLP 파트를 공부할 예정이다. 오늘은 그 중에서도 토큰화(Tokenization)에 대해 공부했다.1. 토큰화(Tokenization)자연어 처리(NLP)는 인공지능의 하위
choiwonjin.tistory.com
이전 포스팅에 이어서 토큰화에 대한 내용을 작성해 보겠습니다.
이번 포스팅의 주제는 서브워드 토큰화입니다.
오늘 사용할 코드: https://github.com/choiwonjini/AIFFEL_practice/blob/main/NLP/1_Tokenization.ipynb
AIFFEL_practice/NLP/1_Tokenization.ipynb at main · choiwonjini/AIFFEL_practice
Contribute to choiwonjini/AIFFEL_practice development by creating an account on GitHub.
github.com
서브워드 토큰화(Subword Tokenization)
현대 NLP에서 텍스트를 기본 단위로 분리하는 핵심 기술이다.
이름 그대로 단어 이하(subword) 수준의 단위로 텍스트를 분리하며, 단어 기반 토큰화와 문자 기반 토큰화의 장점을 절충한 방식이다. (단어 기반 & 문자 기반 토큰화는 위의 이전 포스팅 링크에서 확인할 수 있다.)
단어 기반 토큰화의 문제점
- OOV(Out of Vocabulary) 문제
- love, lovely, loving, loved처럼 형태가 조금씩 다른 파생어를 모두 별개의 단어로 저장해야 한다.
- 그 외에도 신조어, 은어 등으로 인한 문제가 생긴다.
문자 기반 토큰화의 문제점
- 자음/모음 또는 알파벳 단위로 분리하기 때문에 OOV 문제는 해결되지만, 단어 자체가 가진 의미 단위가 사라진다.
- 같은 이유로 토큰들이 너무 많아져서 모델이 처리해야 할 시퀀스 길이가 매우 길어지게 되고, 학습 및 추론 속도가 느려진다.
서브워드 토큰화의 알고리즘으로는 BPE (Byte Pair Encoding), WordPiece 등이 있다.
1. BPE (Byte Pair Encoding)
텍스트 데이터에서 가장 빈번하게 등장하는 글자 쌍의 조합을 찾아 부호화하는 압축 알고리즘이다.
핵심 아이디어: "가장 자주 연속으로 등장하는 쌍(pair)을 찾아 하나로 합친다."
1.1. 알고리즘
어휘 사전 크기(Vocab_size)를 10개로 정한다고 가정하고, 아래와 같은 corpus가 있다고 하자.
("low", 5)
("lowest", 2)
("newer", 6)
("widest", 3)
(1) 초기화(Initialization)
모든 단어를 문자 단위로 분리한다.
빈도 사전:
(l o w, 5)
(l o w e r, 2)
(n e w e s t, 6)
(w i d e s t, 3)
초기 어휘 사전 (V): {'l', 'o', 'w', 'e', 's', 't', 'n', 'i', 'd', 'r'} (기본 문자들)
(2) 가장 빈번한 쌍(Pair) 찾기 및 병합
코퍼스 전체에서 가장 빈번한 연속된 두 토큰의 쌍을 찾는다.
(l, o): 5 (low) + 2 (lower) = 7
(o, w): 5 (low) + 2 (lower) = 7
(w, e): 2 (lower) + 6 (newest) = 8
(e, r): 2 (lower) + 3 (widest) = 5
(n, e): 6 (newest)
(e, w): 6 (newest)
(e, s): 6 (newest) + 3 (widest) = 9
(s, t): 6 (newest) + 3 (widest) = 9
(w, i): 3 (widest)
(i, d): 3 (widest)
(d, e): 3 (widest)
가장 빈번한 쌍은 (e, s)와 (s, t)가 9회로 공동 1위이다.
이 중 하나인 (e, s)을 선택하고 병합해서 하나의 새로운 토큰을 만든다.
병합 1: e + s → es
어휘 사전 추가: (V): {'l', ..., 'r', 'es'}
빈도 사전 업데이트:
(l o w, 5)
(l o w e r, 2)
(n e w es t, 6)
(w i d es t, 3)
(3) 반복
다시 가장 빈번한 쌍을 찾고 병합해서 새로운 토큰을 만든다.
(es, t): 6 + 3 = 9회 (가장 높음)
병합 2: es + t → est
어휘 사전 추가: (V): {'l', ..., 'es', 'est'}
빈도 사전 업데이트:
(l o w, 5)
(l o w e r, 2)
(n e w est, 6)
(w i d est, 3)
이 과정을 Vocab_size(10) 만큼 반복하면 아래의 빈도 사전과 어휘 사전이 생성된다.
빈도 사전:
(low, 5), (low e r, 2), (n e w est, 6), (w i d est, 3)
어휘 사전:
['l', 'o', 'w', 'e', 's', 't', 'n', 'i', 'd', 'r',
'es', 'est', 'lo', 'low', 'ne', 'new', 'newset', 'wi', 'wid', 'widest']
따라서 newer, wider, lowest 처럼 기존 코퍼스에 없는 단어가 입력되더라도 OOV로 처리되지 않고, 어휘 사전을 참고해서
"new e r", "wid e r", "low est"와 같이 토큰화가 가능해진다.
1.2. SentencePiece & Korpora를 활용한 토크나이저 학습
SentencePiece 라이브러리는 구글에서 개발한 오픈소스 서브워드 토크나이저 라이브러리다.
BPE와 유사한 알고리즘을 사용해 토큰화, 단어사전을 생성한다.
또한, WordPiece, 유니코드 기반의 다양한 알고리즘을 지원하며 사용자가 직접 설정할 수 있는 하이퍼파람터들을 제공해 세밀한 토크나이징 기능을 제공한다.
Korpora 라이브러리는 국립국어원이나 AI Hub에서 제공하는 korpus 데이터를 쉽게 사용할 수 있게 제공하는 오픈소스 라이브러리다.
(1) 청와대 청원 데이터 다운로드
# 청와대 청원 데이터
from Korpora import Korpora
corpus = Korpora.load("korean_petitions")
dataset = corpus.train
petition = dataset[0]
print("청원 시작일 :", petition.begin)
print("청원 종료일 :", petition.end)
print("청원 동의 수 :", petition.num_agree)
print("청원 범주 :", petition.category)
print("청원 제목 :", petition.title)
print("청원 본문 :", petition.text[:30])
청원 시작일 : 2017-08-25
청원 종료일 : 2017-09-24
청원 동의 수 : 88
청원 범주 : 육아/교육
청원 제목 : 학교는 인력센터, 취업센터가 아닙니다. 정말 간곡히 부탁드립니다.
청원 본문 : 안녕하세요. 현재 사대, 교대 등 교원양성학교들의 예비
(2) 학습 데이터세트 생성
from Korpora import Korpora
petitions = corpus.get_all_texts()
with open("/content/corpus.txt", "w", encoding="utf-8") as f:
for petition in petitions:
f.write(petition + '\n')
get_all_texts 메서드로 본문 데이터세트를 한 번에 불러올 수 있다.
(3) 토크나이저 모델 학습
from sentencepiece import SentencePieceTrainer
SentencePieceTrainer.Train(
"--input=/content/corpus.txt\
--model_prefix=/content/petition_bpe\
--vocab_size=8000 model_type=bpe"
)
학습이 완료되면 petition_bpe.model 파일과 petition_bpe.vocab 파일이 생성된다.
어휘 사전 파일을 열어보면 "_"가 포함된 데이터를 볼 수 있는데, sentencepiece 라이브러리는 공백도 특수문자로 취급하므로 토큰화 과정에서 "_"로 공백을 표현한다.
ex) "Hello World" → "_Hello + _Wor + ld"로 토큰화된다.
(4) BPE 토큰화
from sentencepiece import SentencePieceProcessor
tokenizer = SentencePieceProcessor()
tokenizer.load("/content/petition_bpe.model")
sentence = "안녕하세요, 토크나이저가 잘 학습되었군요!"
sentences = ["이렇게 입력값을 리스트로 받아서", "쉽게 토크나이저를 사용할 수 있답니다"]
tokenized_sentence = tokenizer.encode_as_pieces(sentence)
tokenized_sentences = tokenizer.encode_as_pieces(sentences)
print("단일 문장 토큰화 :", tokenized_sentence)
print("여러 문장 토큰화 :", tokenized_sentences)
encoded_sentence = tokenizer.encode_as_ids(sentence)
encoded_sentences = tokenizer.encode_as_ids(sentences)
print("단일 문장 정수 인코딩 :", encoded_sentence)
print("여러 문장 정수 인코딩 :", encoded_sentences)
decode_ids = tokenizer.decode_ids(encoded_sentence)
decode_pieces = tokenizer.decode_pieces(encoded_sentences)
print("정수 인코딩에서 문장 변환 :", decode_ids)
print("하위 단어 토큰에서 문장 변환 :", decode_pieces)
단일 문장 토큰화 : ['▁안녕하세요', ',', '▁토', '크', '나', '이', '저', '가', '▁잘', '▁학', '습', '되었', '군요', '!']
여러 문장 토큰화 : [['▁이렇게', '▁입', '력', '값을', '▁리', '스트', '로', '▁받아서'], ['▁쉽게', '▁토', '크', '나', '이', '저', '를', '▁사용할', '▁수', '▁있', '답니다']]
단일 문장 정수 인코딩 : [667, 6553, 994, 6880, 6544, 6513, 6590, 6523, 161, 110, 6554, 872, 787, 6648]
여러 문장 정수 인코딩 : [[372, 182, 6677, 4433, 1772, 1613, 6527, 4162], [1681, 994, 6880, 6544, 6513, 6590, 6536, 5852, 19, 5, 2639]]
정수 인코딩에서 문장 변환 : 안녕하세요, 토크나이저가 잘 학습되었군요!
하위 단어 토큰에서 문장 변환 : ['이렇게 입력값을 리스트로 받아서', '쉽게 토크나이저를 사용할 수 있답니다']
토큰화가 수행됐고, 정수 인코딩과 문장 변환을 자유롭게 할 수 있다.
하지만 영어는 부자연스럽게 토큰화가 된 것을 알 수 있다.
(5) 어휘 사전 불러오기
vocab = {idx: tokenizer.id_to_piece(idx) for idx in range(tokenizer.get_piece_size())}
print(list(vocab.items())[:5]) # 5개만 출력
print("vocab size :", len(vocab))
[(0, '<unk>'), (1, '<s>'), (2, '</s>'), (3, '니다'), (4, '▁이')]
vocab size : 8000
- <unk>: OOV 발생 시 매핑되는 토큰
- <s>, </s>: 문장의 시작 지점과 종료 지점을 표시하는 토큰
2. WordPiece
Wordpiece 토크나이저는 BPE와 유사한 방법으로 학습되지만, 빈도 기반이 아닌 확률 기반으로 글자 쌍을 병합한다.
- BPE: 빈도수가 가장 높은 쌍을 병합
- Wordpiece: 병합했을 때 Likelihood를 가장 높이는 쌍을 병합
즉, 모델이 새로운 서브워드를 생성할 때 이전 서브워드와 함께 나타날 확률을 계산해 가장 높은 확률을 가진 서브워드를 선택하는 방법이다.
수식은 아래와 같다.

- count(A,B): 두 토큰이 함께 등장한 횟수. (BPE와 동일)
- count(A) x count(B): 두 토큰이 각각 독립적으로 등장한 횟수의 곱.
2.1. 알고리즘
BPE와 마찬가지로 아래의 corpus가 있다고 하자.
("low", 5)
("lowest", 2)
("newer", 6)
("widest", 3)
(1) 초기 문자 빈도 계산
먼저, 코퍼스에 있는 모든 개별 문자(토큰)의 총 등장 횟수를 센다.
l: 5 (low) + 2 (lower) = 7
o: 5 (low) + 2 (lower) = 7
w: 5 (low) + 2 (lower) + 6 (newest) + 3 (widest) = 16
e: 2 (lower) + 6 (newest) * 2 + 3 (widest) = 2 + 12 + 3 = 17
r: 2 (lower) = 2
n: 6 (newest) = 6
s: 6 (newest) + 3 (widest) = 9
t: 6 (newest) + 3 (widest) = 9
i: 3 (widest) = 3
d: 3 (widest) = 3
(2) 최고 점수(Score) 쌍 찾기
모든 연속된 쌍의 점수를 계산한다.
(e, s)
count(e, s) = 6 (newest) + 3 (widest) = 9
count(e) = 17
count(s) = 9
Score(e, s) = 9 / (17 * 9) = 1 / 17 ≈ 0.059
(s, t)
count(s, t) = 6 (newest) + 3 (widest) = 9
count(s) = 9
count(t) = 9
Score(s, t) = 9 / (9 * 9) = 1 / 9 ≈ 0.111
(i, d)
count(i, d) = 3 (widest)
count(i) = 3
count(d) = 3
Score(i, d) = 3 / (3 * 3) = 3 / 9 ≈ 0.333
(l, o)
count(l, o) = 5 (low) + 2 (lower) = 7
count(l) = 7
count(o) = 7
Score(l, o) = 7 / (7 * 7) = 1 / 7 ≈ 0.143
결과: Score(i, d) (0.333)가 가장 높다.
병합 1: i + d → id
어휘 사전 추가: (V): {..., 'i', 'd', ..., 'id'}
빈도 사전 업데이트:
(l o w, 5)
(l o w e r, 2)
(n e w e s t, 6)
(w id e s t, 3)
가장 흔한 쌍인 (e, s)의 점수가 가장 높지 않은 이유는?
(e, s)가 가장 흔한 쌍이긴 하지만 e, s 모두 각자 다른 문자와도 자주 결합하므로( (w, e), (e, w), (e, r) ), 이 둘의 결합력이 특별하다고 보지 않는다.
반면 i와 d는 희귀하지만(각 3회), 이 코퍼스 내에서는 100% 확률로 항상 붙어 다니는데, WordPiece는 이 결합력을 높은 점수로 평가한다.
(3) 반복
다음 최고 점수 쌍은 (l, o)이며 'lo'를 어휘 사전에 추가하고, 빈도 사전도 업데이트 한다.
이 과정을 반복해 연속된 글자 쌍이 더 이상 나타나지 않거나 정해진 어휘 사전 크기에 도달할 때까지 학습한다.
2.2. Tokenizers 라이브러리의 wordpiece API를 이용해 토크나이저 학습
토크나이저스 라이브러리는 Normalization과 사전 토큰화(Pre-tokenization)를 제공한다.
Normalization은 텍스트를 표준화하고 모호한 경우를 방지하기 위해 일부 문자를 대체하거나 제거하는 등의 작업을 수행한다.
- 불필요한 공백 제거
- 대소문자 변환
- 유니코드 정규화(인코딩 방식에 따라 동일한 글자가 여러 유니코드로 표현되는 경우가 있음)
- 구두점 처리
- 특수 문자 처리
사전 토큰화는 입력 문장을 토큰화하기 전에 다른 단어와 같은 작은 단위로 나눈 기능을 제공한다.
공백이나 구두점을 기준으로 입력 문장을 나눠 텍스트를 효율적으로 처리할 수 있다.
(1) wordpiece 토크나이저 학습
from tokenizers import Tokenizer
from tokenizers.models import WordPiece
from tokenizers.normalizers import Sequence, NFD, Lowercase
from tokenizers.pre_tokenizers import Whitespace
tokenizer = Tokenizer(WordPiece())
tokenizer.normalizer = Sequence([NFD(), Lowercase()])
tokenizer.pre_tokenizer = Whitespace()
tokenizer.train(["/content/corpus.txt"])
tokenizer.save("/content/petition_wordpiece.json")
워드피스 모델을 불러온 다음 정규화 방식과 사전 토큰화 방식을 설정한다.
- 정규화 방식: NFD 유니코드 정규화, 소문자 변환(Lowercase)
- 사전 토큰화: 공백과 구두점 기준으로 분리
(2) wordpiece 토큰화
# 워드피스 토큰화
from tokenizers import Tokenizer
from tokenizers.decoders import WordPiece as WordPieceDecoder
tokenizer = Tokenizer.from_file("/content/petition_wordpiece.json")
tokenizer.decoder = WordPieceDecoder()
sentence = "안녕하세요, 토크나이저가 잘 학습되었군요!"
sentences = ["이렇게 입력값을 리스트로 받아서", "쉽게 토크나이저를 사용할 수 있답니다"]
encoded_sentence = tokenizer.encode(sentence)
encoded_sentences = tokenizer.encode_batch(sentences)
print("인코더 형식:", type(encoded_sentence))
print("단일 문장 토큰화:", encoded_sentence.tokens)
print("여러 문장 토큰화:", [enc.tokens for enc in encoded_sentences])
print("단일 문장 정수 인코딩:", encoded_sentence.ids)
print("여러 문장 정수 인코딩:", [enc.ids for enc in encoded_sentences])
print("정수 인코딩에서 문장 변환:", tokenizer.decode(encoded_sentence.ids))
인코더 형식: <class 'tokenizers.Encoding'>
단일 문장 토큰화: ['안녕하세요', ',', '토', '##크', '##나이', '##저', '##가', '잘', '학습', '##되었', '##군요', '!']
여러 문장 토큰화: [['이렇게', '입력', '##값을', '리스트', '##로', '받아서'], ['쉽게', '토', '##크', '##나이', '##저', '##를', '사용할', '수', '있다', '##ᆸ니다']]
단일 문장 정수 인코딩: [8760, 11, 8693, 8415, 16269, 7536, 7488, 7842, 15016, 8670, 8734, 0]
여러 문장 정수 인코딩: [[8187, 19643, 13834, 28119, 7495, 12607], [9739, 8693, 8415, 16269, 7536, 7510, 14129, 7562, 8157, 7489]]
정수 인코딩에서 문장 변환: 안녕하세요, 토크나이저가 잘 학습되었군요!
Tokenizer 객체를 생성한 후, WordPieceDecoder()을 사용해 Tokenizer의 디코더를 워드피스 디코더로 설정한다.
또한 인코딩(encode) 메서드로 문장을 토큰화할 수 있으며 batch를 붙여 여러 문장을 한 번에 토큰화할 수도 있다.
회고
오늘은 BPE와 Wordpiece라는 두 가지 알고리즘에 대해 공부했다.
최근 연구 동향은 더 큰 corpus를 사용해 모델을 학습하고 OOV의 위험을 줄이기 위해 서브워드 토큰화를 활용한다고 한다.
두 알고리즘의 방식이 그렇게 복잡한 편은 아니라서 이해하는 데 어려움은 없었다.
토큰화는 이제 마무리하고, 다음 포스팅부터는 임베딩에 대해 작성하겠습니다.
'AI > NLP' 카테고리의 다른 글
| [10/31] 아이펠 리서치 15기 TIL | Transformer (0) | 2025.11.05 |
|---|---|
| [10/29] 아이펠 리서치 15기 TIL | Attention (0) | 2025.11.01 |
| [10/26] 아이펠 리서치 15기 TIL | RNN & LSTM (1) | 2025.10.26 |
| [10/25] 아이펠 리서치 15기 TIL | 임베딩(Embedding) (1) | 2025.10.25 |
| [10/23] 아이펠 리서치 15기 TIL | 토큰화 (Tokenization): 형태소 분석, 품사 태깅 (0) | 2025.10.23 |