
텍스트를 숫자의 배열(텐서)로 변환하는 도구
텍스트를 단어 또는 하위 단어 단위(토큰)로 분리한 뒤 토큰들을 숫자로 변환 또한, 어떤 토큰에 주의를 집중해야 하는지를 알려주는 attention mask도 함께 생성
Transformers에서는 다음 세 가지 하위 단어 기반 토크나이저를 사용함
ex) "Don't you love 🤗 Transformers? We sure do."
["Don't", "you", "love", "🤗", "Transformers?", "We", "sure", "do."]
["Don", "'", "t", "you", "love", "🤗", "Transformers", "?", "We", "sure", "do", "."]
spaCy, Moses 등은 rule-based 토크나이저임
["Do", "n't", "you", "love", "🤗", "Transformers", "?", "We", "sure", "do", "."]
⚠️ 대규모 코퍼스에서는 고유 단어 수가 너무 많아져서, 모델의 임베딩 행렬이 커지고 메모리•속도 효율이 떨어지게 됨
메모리 사용은 줄지만 "t" 같은 글자 하나만으로는 의미를 잡기 어려워 성능이 떨어짐
자주 나오는 단어는 그대로, 드물게 나오는 단어는 의미 있는 부분으로 분해함
ex) "annoyingly" ➡️ "annoying" , "ly"
from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained("google-bert/bert-base-uncased")
tokenizer.tokenize("I have a new GPU!")
# 출력 : ["i", "have", "a", "new", "gp", "##u", "!"]
gpu는 vocabulary에 없으므로 "gp"와 "##u"로 분할하였음. 이때 "##"은 앞 토큰과 붙는다는 뜻
학습 데이터를 단어로 분할하는 pre-tokenizer에 기반함.
① pre-tokenization
② 고유 단어 집합이 만들어지고 각 단어가 학습 데이터에서 몇 번 등장했는지 빈도가 계산됨
③ 고유 단어 집합에 등장한 모든 기호로 구성된 base vocabulary를 만든 뒤, 두 기호를 병합해 새로운 기호를 만드는 병합 규칙을 학습
④ 원하는 vocabulary size(=기본 기호 수 + 병합 회수)에 도달할 때까지 계속
💡 vocabulary size는 토크나이저를 학습하기 전에 설정하는 하이퍼파라미터
프리토크나이징 후 다음과 같은 단어 및 등장 빈도가 있음
("hug", 10), ("pug", 5), ("pun", 12), ("bun", 4), ("hugs", 5)
기본 어휘는 ["b", "g", "h", "n", "p", "s", "u"]가 됨. 각 단어를 기호 단위로 나누면
("h" "u" "g", 10), ("p" "u" "g", 5), ("p" "u" "n", 12), ("b" "u" "n", 4), ("h" "u" "g" "s", 5)
BPE는 가능한 모든 기호 쌍의 빈도를 계산한 뒤, 가장 자주 등장하는 쌍을 선택함
위 예시에서는 "u" 다음 "g"가 총 20회 등장하므로 "ug"가 어휘에 추가됨
("h" "ug", 10), ("p" "ug", 5), ("p" "u" "n", 12), ("b" "u" "n", 4), ("h" "ug" "s", 5)
다음으로 많이 등장한 쌍은 "u"와 "n"이므로 "un"이 추가됨
그 다음은 "h" 와 "ug" 쌍이 많이 등장하므로 "hug"가 어휘에 추가됨
현재 어휘는 다음과 같음 ["b", "g", "h", "n", "p", "s", "u", "ug", "un", "hug"]
("hug", 10), ("p" "ug", 5), ("p" "un", 12), ("b" "un", 4), ("hug" "s", 5)
만약 BPE 학습이 여기서 멈춘다고 가정하자. 학습된 병합 규칙은 새로운 단어에도 적용이된다.
"bug"는 ["b","ug"]로 토크나이징 되지만 "mug"는 "m'이 기본 어휘에 없으므로 ["< unk >","ug"]로 토크나이징 됨
모든 유니코드 문자를 기본 기호로 포함시키면 기본 어휘가 매우 커지므로 이를 해결하기 위해 바이트를 기본 어휘로 사용함. 이렇게 하면 기본 어휘 크기를 256개 바이트로 제한하면서 모든 기호를 처리할 수 있음. GPT-2는 문장 끝 토큰과 50,000개 병합 기호를 추가하여 총 50,257개 어휘를 갖게되고 이 방식 덕분에 < unk > 기호 없이 모든 텍스트 처리 가능
학습 데이터에 등장한 모든 문자를 어휘에 포함시킨 뒤, 정해진 횟수만큼 병합 규칙을 학습함. BPE는 자주 등장하는 쌍을 병합하지만, WordPiece는 병합 시 전체 학습 데이터의 가능도가 가장 증가하는 쌍을 병합함
큰 어휘 집합으로 시작하여 점점 줄여나가는 방식을 사용함
초기 어휘는 미리 토크나이징된 단어들과 자주 등장하는 부분 문자열을 포함함
Transformers에서 직접 사용되지 않지만, SentencePiece와 함께 사용됨
① 학습 과정에서, 현재 어휘와 Unigram언어 모델을 기반으로 loss을 정의함.
② 그 후, 각 기호를 제거했을 때 손실이 얼마나 증가하는지를 계산하고, 손실 증가가 가장 적은 기호들을 제거함.
이 과정을 반복하여 원하는 어휘 크기에 도달함(단, 모든 문자를 포함하는 기본 기호는 항상 유지됨)
Unigram은 병합 규칙이 아닌 확률에 기반하므로, 여러 가지 분할 방식이 가능하고, 각 토큰 조합에 대해 확률을 저장함
전체 손실은 아래와 같이 정의됨
어휘가 ["b", "g", "h", "n", "p", "s", "u", "ug", "un", "hug"]일 때 "hugs"는 다음 세 가지 중에 선택될 수 있음
앞서 설명한 모든 토크나이징 알고리즘은 입력 텍스트가 공백으로 단어를 구분한다고 가정한다는 문제를 가짐. 하지만 중국어, 일본어, 태국어는 단어 구분을 공백으로 하지 않음. 이 문제를 해결하기 위해서 SentencePiece는 입력을 문자열 스트림으로 간주하여, 공백도 하나의 문자로 포함시킴. 이후 BPE 또는 Unigram 알고리즘을 사용해 어휘를 구성함
모든 토크나이저는 PreTrainedTokenizerBase 클래스를 상속받으며 다음과 같은 공통 메서드를 제공함 from_pretrained(), batch_decode()
| 클래스 이름 | 설명 |
|---|---|
| PreTrainedTokenizer | 순수 Python으로 구현됨 |
| PreTrainedTokenizerFast | Rust 기반의 고속 토크나이저 |
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("google/gemma-2-2b")
tokenizer("We are very happy to show you the 🤗 Transformers library", return_tensors="pt")
#결과
{
'input_ids': tensor([[2, 1734, 708, 1508, 4915, 577, 1500, 692, 573, 156808, 128149, 9581, 235265]]),
'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]])
}
⚠️ 항상 사용하는 모델의 vocab와 같은 vocab를 가진 토크나이저를 사용해야 함
( 커스텀 토크나이저를 쓸때 pretrained모델의 vocab와 다르면 문제가 생길 수 있음 )
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("./model_directory/my_vocab_file.txt")
모든 사전학습 모델은 고유한 토크나이저와 vocabulary를 사용하므로 모델을 불러올 때는, 그 모델이 학습할 때 사용했던 토크나이저를 함께 불러오는 것이 중요함
from transformers import GemmaTokenizer
tokenizer = GemmaTokenizer.from_pretrained("google/gemma-2-2b")
tokenizer("We are very happy to show you the 🤗 Transformers library.", return_tensors="pt")
from transformers import GemmaTokenizerFast
tokenizer = GemmaTokenizerFast.from_pretrained("google/gemma-2-2b")
tokenizer("We are very happy to show you the 🤗 Transformers library.", return_tensors="pt")
Rust로 구현된 빠른 토크나이저로 처리 속도가 빠르고, 병렬 처리도 지원됨
from transformers import GemmaTokenizerFast
tokenizer = GemmaTokenizerFast(vocab_file="my_vocab_file.txt")
텍스트 토큰 외에도, 멀티모달 토크나이저는 이미지나 오디오 등 다른 모달리티에서 사용하는 특수 토큰도 함께 관리함. 이러한 토큰들은 토크나이저 객체의 속성으로 등록되어 쉽게 접근할 수 있음
vision_tokenizer = AutoTokenizer.from_pretrained(
"llava-hf/llava-1.5-7b-hf",
extra_special_tokens={
"image_token": "<image>", # 이미지 자리 표시자
"boi_token": "<image_start>", # 이미지 시작
"eoi_token": "<image_end>" # 이미지 끝
}
)
print(vision_tokenizer.image_token, vision_tokenizer.image_token_id)
# 출력: ("<image>", 32000)
vision_tokenizer.save_pretrained("./path/to/tokenizer")
위 코드로 토크나이저를 디스크에 저장하면, 다음에도 같은 특수 토큰들이 적용된 상태로 쉽게 불러올 수 있음.
from tokenizers import Tokenizer
from tokenizers.models import BPE
from tokenizers.trainers import BPETrainer
tokenizer = Tokenizer(BPE(unk_token = "[UNK]"))
trainer = BpeTrainer(special_tokens = ["[UNK]","[CLS]","[SEP]","[PAD]","[MASK]"])
from tokenizers.pre_tokenizers import Whitespace
tokenizer.pre_tokenizer = Whitespace()
공백을 기준으로 토큰을 나누는 전처리(pre-tokenization)을 적용함
files = ["./data/text1.txt", "./data/text2.txt"]
tokenizer.train(files, trainer)
학습시킬 텍스트 파일 리스트를 넘기면 학습이 시작됨
tokenizer.save("tokenizer.json")
from transformers import PreTrainedTokenizerFast
fast_tokenizer = PreTrainedTokenizerFast(tokenizer_object=tokenizer)
from transformers import PreTrainedTokenizerFast
fast_tokenizer = PreTrainedTokenizerFast(tokenizer_file="tokenizer.json")
OpenAI에서 만든 BPE 기반 토크나이저로 여러가지 텍스트 토크나이징 방식을 지원하며, 빠른 속도와 효율적인 메모리 사용이 특징.
현재 titoken으로 학습된 모델은 GPT-2, LLaMA 3
Transformers는 tokenizer.model 형식의 tiktoken 파일을 지원하며, 이를 자동으로 Rust 기반의 PreTrainedTokenizerFast 형식으로 변화해 사용함
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained(
"meta-llama/Meta-Llama-3-8B-Instruct",
subfolder="original"
)
tiktoken의 tokenizer.model 파일은 특수 토큰 정보나 패턴 문자열 정보가 포함되어 있지 않으므로 Transformers에서 쓸 때는 tokenizer.json으로 변환하는 것이 좋음
from transformers.integrations.tiktoken import convert_tiktoken_to_fast
from tiktoken import get_encoding
# GPT-2 토크나이저 로딩 (또는 사용자 정의 encoding도 가능)
encoding = get_encoding("gpt2")
# Transformers 호환 가능한 tokenizer.json으로 변환
convert_tiktoken_to_fast(encoding, "config/save/dir")
결과로 "config/save/dir/tokenizer.json"파일이 생성됨
from transformers import PreTrainedTokenizerFast
tokenizer = PreTrainedTokenizerFast.from_pretrained("config/save/dir")
Transformer 모델은 입력으로 PyTorch, TensorFlow 또는 NumPy 형태의 텐서를 받음. 토크나이저의 역할은 텍스트를 이 텐서 형식으로 전처리한는 것임
return_tensors 파라미터를 사용하여 원하는 프레임워크 형식으로 결과를 반환할 수 있음
문자열 ➡️ 토큰 분리
tokens = tokenizer.tokenize("We are very happy to show you the 🤗 Transformers library")
print(tokens)
# 출력 : ['We', '▁are', '▁very', '▁happy', '▁to', '▁show', '▁you', '▁the', '▁🤗', '▁Transformers', '▁library']
각 토큰을 모델이 이해할 수 있는 숫자 ID로 변환
ids = tokenizer.convert_tokens_to_ids(tokens)
print(ids)
# 출력 : [1734, 708, 1508, 4915, 577, 1500, 692, 573, 156808, 128149, 9581]
모델이 예측한 숫자 ID 배열을 문자열로 변환
decoded_string = tokenizer.decode(ids)
print(decoded_string)
'We are very happy to show you the 🤗 Transformers library'
특수 토큰은 모델이 텍스트의 구조나 의미를 더 잘 이해할 수 잇도록 추가적인 정보를 제공함.
tokenizer()로 텍스트를 직접 처리한 결과와, convert_tokens_to_ids()로 처리한 결과를 비교해 보면 추가된 토큰이 있는 걸 확인할 수 있음
model_inputs = tokenizer("We are very happy to show you the 🤗 Transformers library.")
print(model_inputs["input_ids"])
# 출력: [2, 1734, 708, 1508, ..., 235265]
tokens = tokenizer.tokenize("We are very happy to show you the 🤗 Transformers library.")
ids = tokenizer.convert_tokens_to_ids(tokens)
print(ids)
# 출력: [1734, 708, 1508, ..., 235265]
[2, ...]에서 2번 트콘은 특수 토큰 < bos > (beginning of sentence)를 의미함
이 토큰은 모델에게 '문자이 시작된다'는 신호를 줌
print(tokenizer.decode(model_inputs["input_ids"]))
print(tokenizer.decode(ids))
'<bos>We are very happy to show you the 🤗 Transformers library.'
'We are very happy to show you the 🤗 Transformers library'
모든 모델이 특수 토큰을 필요로 하진 않지만, 필요하다면 tokenizer가 자동으로 추가해 줌
한 문장씩 처리하는 것보다 여러 문장을 한꺼번에 처리하는 것이 훨씬 빠르고 효율적임
batch_sentences = [
"But what about second breakfast?",
"Don't think he knows about second breakfast, Pip.",
"What about elevensies?",
]
encoded_inputs = tokenizer(batch_sentences, return_tensors="pt")
print(encoded_inputs)
# 출력
{
'input_ids': [
[2, 1860, 1212, 1105, 2257, 14457, 235336],
[2, 4454, 235303, 235251, 1742, 693, ..., 235265],
[2, 1841, 1105, 29754, 37453, 235336]
],
'attention_mask': [
[1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1]
]
}
위 출력처럼 문장마다 길이가 다르면, Transformer 모델은 이를 처리할 수 없음. 따라서 모든 문장의 길이를 동일하게 맞추는 패딩이 필요함
encoded_inputs = tokenizer(batch_sentences, padding=True, return_tensors="pt")
print(encoded_inputs)
대부분의 모델은 최대 입력 길이 제한이 있음
너무 긴 문장을 입력하면 모델이 에러를 발생시킬 수 있음
truncation=True를 설정하면 자동으로 길이를 잘라냄
encoded_inputs = tokenizer(batch_sentences, max_length=8, truncation = True, return_tensors = 'pt')
print(encoded_inputs)
max_length를 통해 수동으로 길이 제한