🧩 토큰화란?
토큰화(Tokenization)는 문장이 모델을 처리할 수 있는 토큰 ID 정수 배열로 바꾸는 과정이다. 토크나이저(Tokenizer)는 문장을 토큰 단위로 자르고 정수 배열로 변환해주는 도구다.
Transformer 계열 모델에서는 보통 다음 흐름으로 동작한다.

AutoTokenizer로 토큰화하기
from transformers import AutoTokenizer
model_name = "klue/bert-base"
tokenizer = AutoTokenizer.from_pretrained(model_name)
text = "안녕하세요! 우리집 강아지 이름은 문식이입니다."
# 사람이 읽을 수 있는 토큰 확인
tokens = tokenizer.tokenize(text)
# 토큰을 모델 입력용 정수 ID로 변환
token_ids = tokenizer.encode(text)
print("토큰:")
print(tokens)
print("\n토큰 ID:")
print(token_ids)
print("\nID를 다시 토큰으로 변환:")
print(tokenizer.convert_ids_to_tokens(token_ids))
print("\n복원된 문장:")
print(tokenizer.decode(token_ids))
'''
토큰:
['안녕', '##하', '##세요', '!', '우리', '##집', '강아지', '이름', '##은', '문', '##식', '##이', '##입니다', '.']
토큰 ID:
[2, 5891, 2205, 5971, 5, 3616, 2027, 8522, 3934, 2073, 1091, 2096, 2052, 12190, 18, 3]
ID를 다시 토큰으로 변환:
['[CLS]', '안녕', '##하', '##세요', '!', '우리', '##집', '강아지', '이름', '##은', '문', '##식', '##이', '##입니다', '.', '[SEP]']
복원된 문장:
[CLS] 안녕하세요! 우리집 강아지 이름은 문식이입니다. [SEP]
'''
[CLS]: 문장 전체를 대표하는 용도로 자주 사용되는 시작 토큰[SEP]: 문장 또는 문장 쌍의 구분 토큰[PAD]: 배치 내 문장 길이를 맞추기 위한 패딩 토큰[UNK]: 사전에 없는 토큰
실전 방식: tokenizer(...) 사용
실무에서는 패딩, 길이 제한, 어텐션 마스크 등을 함께 만들 수 있기 때문에 encode()보다 tokenizer() 호출 방식을 주로 사용한다고 한다.
from transformers import AutoTokenizer
model_name = "klue/bert-base"
tokenizer = AutoTokenizer.from_pretrained(model_name)
texts = [
"안녕하세요. 저는 강아지를 키웁니다.",
"이름은 문식이고, 견종은 시츄입니다."
]
encoded = tokenizer(
texts,
padding=True, # 짧은 문장 뒤를 [PAD]로 채움
truncation=True, # 너무 길면 자름
max_length=32,
return_tensors="pt" # PyTorch 텐서로 반환
)
print(encoded)
print("\ninput_ids:")
print(encoded["input_ids"])
print("\nattention_mask:")
print(encoded["attention_mask"])
'''
{'input_ids': tensor([[ 2, 5891, 2205, 5971, 18, 1535, 2259, 8522, 2138, 1,
18, 3, 0, 0, 0, 0],
[ 2, 3934, 2073, 1091, 2096, 2052, 2088, 16, 586, 2033,
2073, 1325, 2979, 12190, 18, 3]]), 'token_type_ids': tensor([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]), 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]])}
input_ids:
tensor([[ 2, 5891, 2205, 5971, 18, 1535, 2259, 8522, 2138, 1,
18, 3, 0, 0, 0, 0],
[ 2, 3934, 2073, 1091, 2096, 2052, 2088, 16, 586, 2033,
2073, 1325, 2979, 12190, 18, 3]])
attention_mask:
tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]])
'''
- input_ids: 각 토큰에 대응하는 정수 ID
- attention_mask: 실제 토큰은 1, 패딩 토큰은 0
- token_type_ids: 문장 A/B 구분용 ID. BERT 문장쌍 입력에서 주로 사용
tokenizer.decode()를 사용해 id값을 원래 토큰 문자열로 복원할 수 있다.
from transformers import AutoTokenizer
model_name = "klue/bert-base"
tokenizer = AutoTokenizer.from_pretrained(model_name)
text = input().strip()
tokens = tokenizer.tokenize(text)
tids = tokenizer.convert_tokens_to_ids(tokens)
print(tids)
for tid in tids:
print(tokenizer.decode([tid]))

🧩 BERT 토크나이저 vs Byte-level BPE 토크나이저
위에서 사용한 klue/bert-base는 비교적 퇴물(?)인 BERT 토크나이저에 해당한다. 반면 최신 LLM들은 대부분 Byte-Level BPE를 사용한다.
- BERT 토크나이저: WordPiece 기반
- Byte-level BPE 토크나이저: UTF-8바이트 단위에서 시작하는 Byte-level BPE 기반
BERT 토크나이저: WordPiece 방식
문장을 먼저 분리한 뒤 단어를 사전에 있는 subword 단위로 나눈다. (예: playing → ["play", "##ing"]) Vocalbulary는 Word Piece 알고리즘으로 생성한다.

Byte-level BPE 토크나이저
Byte-level BPE는 UTF-8 바이트 단위로 Vocalburary를 표현하는 알고리즘이다. 기본적으로 모든 텍스트를 바이트 단위까지 표현할 수 있으므로 이론상 어떤 유니코드 문자도 처리할 수 있다.

WordPiece vs Byte-Level BPE
WordPiece 알고리즘은 사전에 정의된 단어 이외의 문자를 처리할 수 없지만, Byte-level BPE는 모르는 문자이더라도 처리할 수 있다.
from transformers import AutoTokenizer
bert_tokenizer = AutoTokenizer.from_pretrained("klue/bert-base")
byte_level_tokenizer = AutoTokenizer.from_pretrained("roberta-base")
texts = [
"Hello world!",
"안녕하세요, 반갑습니다!",
"새로운이모지😊와 특수문자𓀀",
"쀍뛝쒥뙖"
]
for text in texts:
print("=" * 70)
print("입력 문장:", text)
bert_tokens = bert_tokenizer.tokenize(text)
byte_tokens = byte_level_tokenizer.tokenize(text)
print("\n[BERT WordPiece]")
print(bert_tokens)
print("\n[RoBERTa Byte-Level BPE]")
print(byte_tokens)

위 출력에서 BERT 토크나이저의 경우 Vocalburary에 정의되지 않은 문자나 단어 조합을 처리하지 못해 [UNK] 토큰을 만들지만, Byte-Level BPE의 경우에는 아무 문제 없이 처리하는 것을 확인할 수 있다.
반면 한국어의 경우 어미와 조사가 다양한 특성상 영어 중심으로 학습된 Byte-Level BPE 모델보다 한국어 특화 BERT 모델을 사용하는 것이 더 좋은 결과를 만들 수 있다.
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("./tokenizer_deepseek") # Byte-level BPE 사용
text_kor = "우리집 멍멍이 이름은 문식이"
text_eng = "My puppy's name is Munsik"
def tokenize(text):
tokens = tokenizer.tokenize(text)
tids = tokenizer.convert_tokens_to_ids(tokens)
print(tids)
for tid in tids:
print(tokenizer.decode([tid]), end="")
tokenize(text_eng)
print("\n" + "-" * 20)
tokenize(text_kor)

위 출력을 보면 영문 텍스트와는 달리 한글 텍스트에서 멍에 해당하는 문자열이 ��로 깨져서 나온 것을 확인할 수 있다. 이는 한 글자당 1바이트인 알파벳과는 달리, 한글은 한 글자당 3바이트이기 때문에 억지로 잘라서 토큰화를 하는 과정에서 잘리기 때문이다.

텍스트 깨짐을 해결하려면?
멍멍이가 ����이로 깨져서 출력되는 것은 모델이 한글을 잘못 생성한 것이 아니라 UTF-8 바이트 조각을 토큰별로 너무 일찍 디코딩했기 때문이다. 즉, 한 바이트씩 독립적으로 출력했기 때문에 유효한 한글 문자가 출력되지 않은 것이며, 모든 바이트 조각을 합친 뒤 디코딩하면 유효한 한글 문자가 출력될 수 있다.
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
model_name = "Qwen/Qwen3-0.6B"
tokenizer = AutoTokenizer.from_pretrained(
model_name,
trust_remote_code=True
)
model = AutoModelForCausalLM.from_pretrained(
model_name,
trust_remote_code=True,
torch_dtype=torch.bfloat16,
device_map="auto"
)
prompt = "한국에서 제일 큰 놀이공원이 어디야?"
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
with torch.no_grad():
output_ids = model.generate(
**inputs,
max_new_tokens=100,
temperature=0.7,
top_p=0.8,
)
# 프롬프트 부분을 제외한 새 생성 토큰만 추출
generated_ids = output_ids[0, inputs["input_ids"].shape[1]:]
# 한 바이트씩 decode → 한국어 깨짐 발생!
for token_id in generated_ids:
text = tokenizer.decode([token_id])
print(text, end="", flush=True)
print("\n" + "-" * 20)
# 전체 토큰 시퀀스를 한 번에 decode
answer = tokenizer.decode(
generated_ids,
skip_special_tokens=True,
clean_up_tokenization_spaces=False
)
print(answer)

🧩 임베딩이란?

임베딩은 텍스트, 이미지 및 오디오와 같은 객체를 연속 벡터 공간의 점으로 표현하는 수단이다. (출처: IBM)
임베딩(Embedding)은 토큰 ID를 연속적인 실수 벡터로 변환하는 과정이다. 정확히는 의미 공간 벡터 공간에 단어를 벡터값으로 바꿔 매핑하는 것이다.

더 자세한 설명은 아래 글을 참조하자.
[내일배움캠프] RAG
🧩 임베딩과 벡터 저장소임베딩(Embedding)임베딩은 텍스트, 이미지, 오디오와 같은 비정형 데이터를 컴퓨터가 이해할 수 있는 부동 소수점 숫자의 배열(벡터)로 변환하는 과정을 의미한다. 굳이
munsik22.tistory.com
토큰 ID와 임베딩의 차이
토큰화 결과인 input_ids는 크기나 거리(차이)에 의미가 없는 단순한 정수다. ID가 200인 단어가 ID가 100인 단어보다 ID가 199인 단어에 비슷하다는 의미가 아니다.
임베딩 모델은 내부의 임베딩 테이블을 사용해 ID를 고차원 벡터로 변환한다. 벡터값은 크기나 거리에 의미를 가지며, 값이 비슷할수록 의미가 유사하다고 할 수 있다. BERT Base 계열 모델의 경우 일반적으로 한 토큰을 768차원 벡터로 표현한다.
임베딩 실습
import torch
from transformers import AutoTokenizer, AutoModel
model_name = "klue/bert-base"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModel.from_pretrained(model_name)
text = "우리집 강아지 이름은 문식이"
inputs = tokenizer(text, return_tensors="pt")
with torch.no_grad():
outputs = model(**inputs)
# 첫 번째 토큰([CLS])의 벡터를 문장 벡터로 사용
sentence_embedding = outputs.last_hidden_state[:, 0, :]
print(sentence_embedding.shape) # torch.Size([1, 768])

임베딩 벡터 시각화
import numpy as np
import torch
import plotly.graph_objects as go
from sklearn.decomposition import PCA
from transformers import AutoTokenizer, AutoModel
# --------------------------------------------------
# 1. 데이터
# --------------------------------------------------
texts = [
"인공지능 기술이 발전하고 있습니다.",
"딥러닝은 신경망 기반의 학습 방법입니다.",
"파이썬으로 자연어 처리를 공부합니다.",
"오늘 점심은 김치찌개를 먹었습니다.",
"맛있는 커피를 마시고 싶습니다.",
"축구 경기가 매우 흥미로웠습니다.",
"야구 선수의 홈런 기록입니다.",
"Transformer는 문맥 정보를 활용합니다."
]
labels = [
"AI",
"AI",
"AI",
"Food",
"Food",
"Sports",
"Sports",
"AI"
]
# --------------------------------------------------
# 2. Hugging Face 모델 및 토크나이저 로드
# --------------------------------------------------
model_name = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModel.from_pretrained(model_name)
model.eval()
# --------------------------------------------------
# 3. 토큰화 및 문장 임베딩 생성
# --------------------------------------------------
inputs = tokenizer(
texts,
padding=True,
truncation=True,
max_length=128,
return_tensors="pt"
)
with torch.no_grad():
outputs = model(**inputs)
# 토큰별 임베딩
token_embeddings = outputs.last_hidden_state
# 패딩을 제외한 Mean Pooling
attention_mask = inputs["attention_mask"]
mask = attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float()
sentence_embeddings = torch.sum(
token_embeddings * mask,
dim=1
) / torch.clamp(mask.sum(dim=1), min=1e-9)
# Tensor -> NumPy
embeddings = sentence_embeddings.cpu().numpy()
print("임베딩 shape:", embeddings.shape)
# 예: (8, 384)
# --------------------------------------------------
# 4. 임베딩을 3차원으로 축소
# --------------------------------------------------
pca = PCA(n_components=3)
points_3d = pca.fit_transform(embeddings)
# --------------------------------------------------
# 5. 3차원 점을 구 표면으로 정규화
# --------------------------------------------------
radius = 1.0
norms = np.linalg.norm(points_3d, axis=1, keepdims=True)
norms = np.where(norms == 0, 1e-12, norms)
sphere_points = radius * points_3d / norms
x = sphere_points[:, 0]
y = sphere_points[:, 1]
z = sphere_points[:, 2]
# --------------------------------------------------
# 6. 구 표면 생성
# --------------------------------------------------
theta = np.linspace(0, 2 * np.pi, 80)
phi = np.linspace(0, np.pi, 40)
theta_grid, phi_grid = np.meshgrid(theta, phi)
sphere_x = radius * np.cos(theta_grid) * np.sin(phi_grid)
sphere_y = radius * np.sin(theta_grid) * np.sin(phi_grid)
sphere_z = radius * np.cos(phi_grid)
# --------------------------------------------------
# 7. Plotly 3D 시각화
# --------------------------------------------------
fig = go.Figure()
# 반투명 구
fig.add_trace(
go.Surface(
x=sphere_x,
y=sphere_y,
z=sphere_z,
opacity=0.12,
colorscale=[
[0.0, "rgb(180, 220, 255)"],
[1.0, "rgb(180, 220, 255)"]
],
showscale=False,
hoverinfo="skip"
)
)
# 그룹별 색상
colors = {
"AI": "#636EFA",
"Food": "#EF553B",
"Sports": "#00CC96"
}
# 라벨별 점 표시
for label in sorted(set(labels)):
indices = [i for i, value in enumerate(labels) if value == label]
fig.add_trace(
go.Scatter3d(
x=x[indices],
y=y[indices],
z=z[indices],
mode="markers",
name=label,
customdata=np.array([texts[i] for i in indices]).reshape(-1, 1),
marker=dict(
size=8,
color=colors.get(label, "#AAAAAA"),
opacity=0.95,
line=dict(color="white", width=0.5)
),
hovertemplate=(
"<b>분류:</b> " + label + "<br>"
"<b>문장:</b> %{customdata[0]}<br>"
"<b>x:</b> %{x:.3f}<br>"
"<b>y:</b> %{y:.3f}<br>"
"<b>z:</b> %{z:.3f}"
"<extra></extra>"
)
)
)
# 원점
fig.add_trace(
go.Scatter3d(
x=[0],
y=[0],
z=[0],
mode="markers",
name="Origin",
marker=dict(size=4, color="black"),
hoverinfo="skip"
)
)
fig.update_layout(
title="Sentence Embeddings on a 3D Sphere",
width=900,
height=800,
scene=dict(
xaxis=dict(title="PCA 1", range=[-1.2, 1.2]),
yaxis=dict(title="PCA 2", range=[-1.2, 1.2]),
zaxis=dict(title="PCA 3", range=[-1.2, 1.2]),
aspectmode="cube",
camera=dict(eye=dict(x=1.5, y=1.5, z=1.2))
)
)
fig.show()

'CS > Python' 카테고리의 다른 글
| 파이썬으로 전이학습 찍먹하기 (0) | 2026.08.14 |
|---|---|
| 파이썬으로 Attention 찍먹하기 (0) | 2026.08.12 |
| 파이썬으로 MLP 찍먹하기 (0) | 2026.08.11 |
| 파이썬의 PyTorch 라이브러리에 대해 알아보기 (0) | 2026.08.10 |
| 파이썬으로 모델 검증/해석 찍먹하기 (0) | 2026.08.07 |