Danh sách bài viết

Bài 2: Tokenization — vì sao LLM chia chữ thành token

Tokenization là gì và vì sao LLM cần chia text thành token. So sánh 3 chiến lược: word-level, character-level và subword. Quan hệ token vs từ, ảnh hưởng tới context window và chi phí API, đặc thù tiếng Việt và CJK. Code Python với tiktoken và HuggingFace tokenizer.

25/05/2026
13 phút đọc
1 lượt xem
1

Mục tiêu bài học

Sau bài học, bạn sẽ:

  • Hiểu tokenization là bước đầu tiên trong pipeline xử lý text của LLM và vì sao không thể bỏ qua.
  • Phân biệt 3 chiến lược tokenization: word-level, character-levelsubword; biết vì sao subword là chuẩn de-facto hiện nay.
  • Đếm được token bằng tiktoken (OpenAI) và transformers (HuggingFace).
  • Hiểu vì sao 1 từ tiếng Việt thường tốn nhiều token hơn 1 từ tiếng Anh tương đương.
  • Biết ý nghĩa của các special token <bos>, <eos>, <pad>, <unk> và các marker chat như <|im_start|>.
  • Hiểu liên hệ giữa số token với context windowcost khi gọi API LLM.

Bài này là nền cho Bài 3 (BPE detail) và Bài 4 (SentencePiece / WordPiece). Tạm thời chỉ cần hiểu "token là gì, đếm token thế nào" — chưa cần biết thuật toán build vocab.

2

Tokenization là gì

Tokenization là quá trình chia 1 chuỗi text thành các đơn vị nhỏ gọi là token — đây là đơn vị nhỏ nhất mà LLM xử lý.

Ví dụ với câu "Tokenization is fun!":

  • Chia theo từ: ["Tokenization", "is", "fun", "!"] — 4 token.
  • Chia theo subword (kiểu GPT-4): ["Token", "ization", " is", " fun", "!"] — 5 token.
  • Chia theo character: ["T", "o", "k", "e", ...] — 20 token.

Mỗi token sau khi chia sẽ được gán 1 integer ID dựa trên vocabulary mà tokenizer được train sẵn. ID này mới là thứ được đưa vào model.

3

Vì sao LLM cần token, không phải chữ raw

LLM là 1 mạng neural — đầu vào và đầu ra đều là số, không phải ký tự. Quy trình:

  1. Text → token: dùng tokenizer chia chuỗi.
  2. Token → integer ID: tra trong vocabulary (token "hello" có thể là ID 15496).
  3. ID → embedding vector: mỗi ID map sang 1 vector chiều dài thường 768 / 1024 / 4096 (sẽ học ở Bài 6).
  4. Vector → forward pass qua các Transformer block.

Tokenizer là cầu nối giữa text con người và biểu diễn số mà model hiểu được. Cùng 1 model nhưng dùng tokenizer khác → output sai hoàn toàn vì ID không khớp với weight.

4

Word-level tokenization

Chiến lược đơn giản nhất: mỗi từ = 1 token, chia theo khoảng trắng (whitespace) và dấu câu.

Ưu điểm:

  • Trực giác, dễ hiểu — gần với cách con người đọc.
  • Mỗi token có ý nghĩa rõ ràng (1 từ = 1 khái niệm).

Nhược điểm:

  • Vocabulary rất lớn: tiếng Anh có hơn 1 triệu từ khi tính cả biến thể. Mỗi từ trong vocab cần 1 embedding vector → ngốn nhiều bộ nhớ.
  • Out-of-Vocabulary (OOV): từ mới (slang, tên riêng, từ kỹ thuật) không có trong vocab → tokenizer phải gán <unk>, model mất thông tin.
  • Biến thể spelling: run, runs, running, ran bị coi là 4 token hoàn toàn khác nhau — model không biết chúng có gốc chung.
  • Không xử lý được dấu nối, ngôn ngữ không có khoảng trắng (tiếng Trung, Nhật).

Word-level từng phổ biến với các model NLP đời trước (word2vec, GloVe). LLM hiện nay không dùng nữa.

5

Character-level tokenization

Chiến lược ngược lại: mỗi ký tự = 1 token.

Ưu điểm:

  • Vocab rất nhỏ — khoảng 100-300 ký tự nếu chỉ tính ASCII + dấu, hoặc vài nghìn nếu thêm Unicode.
  • Không có OOV — mọi text đều có thể biểu diễn được.
  • Xử lý được sai chính tả, tên riêng, language mới mà không cần retrain vocab.

Nhược điểm:

  • Sequence dài: 1 câu 100 từ ≈ 500-700 ký tự → sequence length gấp ~5-7 lần so với word-level. Attention có độ phức tạp \( O(n^2) \) theo sequence length nên cost tăng nhanh.
  • Mỗi token ít ngữ nghĩa — model phải "tự học" gộp các ký tự lại thành từ. Cần nhiều layer và data hơn để đạt cùng chất lượng.

Character-level vẫn được dùng ở 1 số model nhỏ và pre-tokenization step, nhưng không phải mainstream cho LLM.

6

Subword tokenization — chuẩn hiện nay

Subword = mỗi token là 1 đoạn ngắn (sub-word piece), thường ngắn hơn 1 từ và dài hơn 1 ký tự. Đây là cách cân bằng giữa 2 thái cực ở trên.

Ý tưởng: các từ phổ biến (the, and, hello) giữ nguyên 1 token; các từ ít gặp hoặc dài bị tách thành nhiều subword (unbelievableun + believ + able).

Ưu điểm:

  • Vocab hợp lý: khoảng 10K–100K token (GPT-4 dùng 100,277).
  • Không có OOV — từ chưa thấy bao giờ vẫn tách được thành các subword đã có.
  • Capture được morphology: play, playing, played share token play.
  • Sequence length vừa phải — ngắn hơn character, dài hơn word một chút.

4 thuật toán chính (chi tiết ở Bài 3-4):

  • BPE (Byte-Pair Encoding) — GPT-2/3/4, Llama. Sẽ deep dive ở Bài 3.
  • WordPiece — BERT, DistilBERT. Bài 4.
  • SentencePiece — T5, mBART, LLaMA tokenizer cũ. Bài 4.
  • Unigram — XLNet, ALBERT. Variant của SentencePiece.
7

Ví dụ tokenization thực tế

Với tokenizer subword kiểu GPT, các ví dụ phổ biến:

"playing"        → ["play", "ing"]
"unbelievable"   → ["un", "believ", "able"]
"tokenization"   → ["token", "ization"]
"GPT-4"          → ["G", "PT", "-", "4"]
"Hello, world!"  → ["Hello", ",", " world", "!"]

Lưu ý ở câu cuối: GPT-style tokenizer xử lý khoảng trắng đứng trước từ như 1 phần của token (" world" chứ không phải "world"). Đây là khác biệt với BERT-style — BERT dùng prefix ## cho subword nối tiếp.

Bảng so sánh ngắn:

Câu                  GPT-4 (BPE)             BERT (WordPiece)
"unbelievable"       un, bel, ievable        un, ##bel, ##ie, ##va, ##ble
"playing"            play, ing               play, ##ing
"hello world"        hello,  world           hello, world

Cách chia phụ thuộc vocab đã train, nên cùng 1 từ qua 2 tokenizer khác nhau ra kết quả khác hẳn.

8

Token KHÔNG bằng từ

Đây là hiểu lầm phổ biến nhất khi mới làm việc với LLM. Token và từ không phải là 1:

  • 1 từ có thể tách thành nhiều token (đặc biệt với từ hiếm, tên riêng, language khác).
  • 1 token có thể chỉ là dấu câu, khoảng trắng, hay 1 ký tự đơn lẻ.
  • 1 token có thể chứa cả khoảng trắng đứng trước (" the").

Quy tắc rule-of-thumb của OpenAI cho tiếng Anh:

1,000 token ≈ 750 từ
1 token     ≈ 4 ký tự (tiếng Anh)

Quy tắc này CHỈ đúng cho tiếng Anh và prose tự nhiên. Với code, JSON, hay tiếng Việt thì tỷ lệ khác hẳn (xem mục tiếp).

9

Tokenization cho non-English

Tokenizer của các model phổ biến (GPT, Claude, Llama) được train trên corpus thiên về tiếng Anh, nên ngôn ngữ khác bị thiệt thòi:

Tiếng Việt:

  • Diacritics (dấu thanh, dấu mũ) khiến mỗi âm tiết bị chia thành nhiều byte UTF-8 → nhiều token hơn.
  • Từ ghép ("công nghệ thông tin") thường bị chia rời từng âm tiết thay vì gộp.
  • Trung bình 1 từ tiếng Việt ≈ 2-3 token GPT-4 (so với ~1.3 token cho từ tiếng Anh).

CJK (Chinese, Japanese, Korean):

  • Không có khoảng trắng giữa từ → tokenizer dựa hoàn toàn vào subword/byte-level.
  • Mỗi ký tự Hán/Kana thường chiếm 1-2 token (vì UTF-8 cần 3 byte cho mỗi ký tự CJK).

Hệ quả thực tế: cùng 1 nội dung, prompt tiếng Việt hoặc tiếng Trung tốn 2-3 lần token so với tiếng Anh. Ngân sách API và context window đều bị "đắt" hơn cho language khác — đây là 1 dạng bias inherent của tokenizer.

10

Code thử với tiktoken (OpenAI)

tiktoken là tokenizer chính thức của OpenAI (cài: pip install tiktoken).

import tiktoken

enc = tiktoken.encoding_for_model("gpt-4")

text = "Hello, world!"
tokens = enc.encode(text)
print(tokens)         # [9906, 11, 1917, 0]
print(len(tokens))    # 4

# Decode ngược
decoded = enc.decode(tokens)
print(decoded)        # "Hello, world!"

# Decode từng token để xem hình thù
for tid in tokens:
    print(tid, repr(enc.decode([tid])))
# 9906 'Hello'
# 11   ','
# 1917 ' world'
# 0    '!'

Mỗi encoding_for_model map sang 1 vocab cụ thể. GPT-4 và GPT-3.5-turbo dùng cl100k_base (100,277 token); GPT-4o dùng o200k_base (~200K token, gộp tiếng non-English tốt hơn).

11

Code thử với HuggingFace tokenizer

Với model open-source (Llama, Mistral, BERT...), dùng transformers:

from transformers import AutoTokenizer

# Llama-3 tokenizer (cần access token HF nếu model gated)
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Meta-Llama-3-8B")

text = "Hello, world!"
ids = tokenizer.encode(text)
print(ids)                              # [128000, 9906, 11, 1917, 0]
print(tokenizer.convert_ids_to_tokens(ids))
# ['<|begin_of_text|>', 'Hello', ',', ' world', '!']

# Decode lại (giữ special token để thấy)
print(tokenizer.decode(ids, skip_special_tokens=False))
# "<|begin_of_text|>Hello, world!"

Để ý: AutoTokenizer tự thêm <|begin_of_text|> (BOS token của Llama-3) ở đầu. tiktoken thì không tự thêm BOS — bạn phải tự xử lý khi format chat.

12

Tokenizer artifact — bug "strawberry"

Câu hỏi "How many R's are in 'strawberry'?" từng làm GPT-4 trả lời sai (nói có 2 R, thực ra là 3). Nguyên nhân không phải model "ngu" — mà do tokenizer.

Với cl100k_base:

enc.encode("strawberry")
# [56664, 15717]  →  [' straw', 'berry']

Model "nhìn" thấy [straw, berry] chứ không thấy [s, t, r, a, w, b, e, r, r, y]. Đếm ký tự bên trong 1 token không phải là phép toán mà model thực hiện natively — nó phải dựa vào kiến thức học được về spelling của từng từ.

Các task tương tự gặp khó vì cùng lý do:

  • Reverse 1 chuỗi.
  • Đếm số ký tự / nguyên âm.
  • Phép toán với số có nhiều chữ số (tokenizer có thể chia "12345" thành [123, 45] hoặc thành từng số).

Bài học: khi cần thao tác ký tự, nên cho LLM gọi tool (Python interpreter) thay vì làm trực tiếp.

13

Special token: BOS, EOS, PAD, UNK

Ngoài token nội dung, vocab còn chứa các special token dùng làm marker:

  • <bos> (Beginning Of Sequence): đánh dấu đầu chuỗi. Llama-3 dùng <|begin_of_text|>.
  • <eos> (End Of Sequence): tín hiệu kết thúc. Model học khi nào nên generate token này để dừng.
  • <pad> (Padding): nhét cho các sequence ngắn đủ chiều dài khi batch — model phải mask để không attend vào pad.
  • <unk> (Unknown): fallback cho ký tự / byte không có trong vocab. Với tokenizer byte-level (BPE của GPT), <unk> gần như không xuất hiện vì mọi byte đều biểu diễn được.

Convention đặt tên thay đổi theo model: <s> (RoBERTa), [CLS]/[SEP] (BERT), <|endoftext|> (GPT-2/3), <|eot_id|> (Llama-3). Đọc tokenizer.special_tokens_map để biết chính xác.

14

Special token cho chat

Model chat (instruction-tuned) thêm các token đánh dấu role:

  • ChatML (OpenAI, Qwen): <|im_start|>user\n...<|im_end|> rồi <|im_start|>assistant.
  • Llama-3: <|start_header_id|>user<|end_header_id|>\n\n...<|eot_id|>.
  • Mistral / Llama-2: [INST] ... [/INST].
  • Gemma: <start_of_turn>user\n...<end_of_turn>.

Sai format chat = model output rác. Lý do nên dùng tokenizer.apply_chat_template() của HuggingFace thay vì tự ghép string:

messages = [
    {"role": "system", "content": "You are a helpful assistant."},
    {"role": "user", "content": "Hello!"},
]
prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
print(prompt)

Hàm này tự lấy đúng template gắn với model — không cần nhớ marker của từng dòng họ.

15

Tokenization và context window

Context window — số lượng token tối đa model xử lý được trong 1 lượt — luôn đo bằng token, không phải từ hay ký tự.

Một số mốc thường gặp (2025):

  • GPT-4 / GPT-4 Turbo / GPT-4o: 128K token.
  • GPT-4.1: 1M token.
  • Claude Sonnet 4 / Opus 4: 200K standard, 1M cho bản context dài.
  • Gemini 1.5 / 2.0 Pro: 1M-2M token.
  • Llama 3 (base): 8K. Llama 3.1 (long-context fine-tune): 128K.

Khi tính budget context, luôn quy đổi qua token, không qua từ. 1 file PDF 50 trang tiếng Việt có thể chạm 30-40K token dễ dàng — chia nhỏ khi cần (sẽ học chunking ở module RAG).

Context window deep dive: xem Bài 7.

16

Chi phí API tính bằng token

Các API LLM (OpenAI, Anthropic, Google) tính tiền theo công thức:

cost = input_tokens × price_in + output_tokens × price_out

Một số điểm cần nhớ:

  • Giá output luôn đắt hơn input (thường gấp 3-5 lần) vì generate token tốn compute hơn process.
  • System prompt được tính vào input mỗi request — system prompt dài 2K token gửi 1000 lần = 2M token tính tiền.
  • Prompt caching (OpenAI, Anthropic) giảm giá phần input lặp lại — vẫn tính theo token.
  • Với task lặp đi lặp lại, ước lượng token trước khi gọi API bằng len(enc.encode(prompt)) để tránh bill bất ngờ.

Tối ưu cost chi tiết: Bài 34.

17

Encode và decode reverse

Tokenizer là map bi-directional: text ↔ token ID.

  • Encode: text → list of IDs — dùng khi chuẩn bị input cho model.
  • Decode: list of IDs → text — dùng khi đọc output từ model.

Với tokenizer byte-level (BPE của GPT, Llama), encode rồi decode lại phải luôn ra đúng text gốc (lossless). Tokenizer WordPiece (BERT) có thể không lossless vì lowercase, strip accent.

text = "Tôi học AI"
ids = enc.encode(text)
back = enc.decode(ids)
assert back == text  # OK với cl100k_base / o200k_base

Lưu ý: khi decode mảng IDs có chứa special token, mặc định nhiều tokenizer sẽ strip chúng. Truyền skip_special_tokens=False nếu muốn debug.

18

So sánh tokenizer trên cùng 1 câu

Đoạn code dưới đây tokenize 1 câu tiếng Anh và 1 câu tiếng Việt bằng 3 tokenizer khác nhau, in số token:

import tiktoken
from transformers import AutoTokenizer

gpt4 = tiktoken.encoding_for_model("gpt-4")
llama = AutoTokenizer.from_pretrained("meta-llama/Meta-Llama-3-8B")
bert = AutoTokenizer.from_pretrained("bert-base-uncased")

sentences = [
    "Tokenization is the first step in any LLM pipeline.",
    "Tokenization là bước đầu tiên trong mọi pipeline của LLM.",
]

for s in sentences:
    print(f"\nText: {s}")
    print(f"  GPT-4  : {len(gpt4.encode(s))} tokens")
    print(f"  Llama-3: {len(llama.encode(s, add_special_tokens=False))} tokens")
    print(f"  BERT   : {len(bert.encode(s, add_special_tokens=False))} tokens")

Output điển hình:

Text: Tokenization is the first step in any LLM pipeline.
  GPT-4  : 11 tokens
  Llama-3: 11 tokens
  BERT   : 11 tokens

Text: Tokenization là bước đầu tiên trong mọi pipeline của LLM.
  GPT-4  : 22 tokens
  Llama-3: 20 tokens
  BERT   : 28 tokens

Câu tiếng Việt dài gấp ~2 lần câu tiếng Anh tương đương khi đo bằng token — confirm bias đã nói ở Bước 9. BERT tệ nhất vì vocab gốc gần như chỉ tiếng Anh, mọi ký tự Việt rơi vào subword UTF-8 ngắn.

19

Pitfall thường gặp

  • Quên BOS / EOS khi format chat: nhiều base model (Llama, Mistral) bắt buộc có BOS ở đầu — quên thì output đầu tiên sẽ lệch. Dùng apply_chat_template() để tránh.
  • Trộn tokenizer khác model: gọi Llama-3-8B với tokenizer của Llama-2 = output garbage. Mỗi model phải đi với tokenizer của chính nó.
  • Coi 1 từ = 1 token khi tính cost / context — thường underestimate 30-50% cho tiếng Anh và gấp đôi cho tiếng Việt.
  • Ước lượng độ dài bằng len(string): chỉ dùng cho draft thô. Quyết định thực tế (cắt context, batch) phải dùng len(tokenizer.encode(text)).
  • Special token bị strip khi log: decode mặc định ẩn <|eot_id|>, dễ debug sai. Truyền skip_special_tokens=False khi cần raw.
  • Tokenize sai whitespace: GPT tokenizer phân biệt "world"" world" — copy-paste prompt có thể tạo ra IDs khác bản gốc.
20

Bài tập

Bài 1: Cài tiktoken, encode chuỗi "Hello, AI Engineer!" với model gpt-4. In ra list IDs và số token. Decode từng ID để xem token thực sự là gì.

Bài 2: Tokenize 2 từ "tokenization""tokenize" bằng cl100k_base. Chúng có share prefix token không? Vì sao?

Bài 3: Lấy 1 đoạn 20 từ tiếng Việt (ví dụ 1 câu trong sách giáo khoa), tokenize bằng GPT-4 và Llama-3. So với 1 câu tiếng Anh tương đương — tỷ lệ token / từ khác nhau bao nhiêu?

Bài 4 (gợi ý): Viết function estimate_cost(prompt, n_output_tokens, price_in, price_out) nhận text prompt, ước lượng tổng cost (USD) cho 1 lần gọi API GPT-4o. Test với prompt 500 từ và 200 token output.

21

Tóm tắt

  • Tokenization là bước chia text thành token — đơn vị nhỏ nhất LLM xử lý. Sau đó token → integer ID → embedding vector.
  • 3 chiến lược: word-level (vocab khổng lồ, OOV), character-level (sequence dài), subword (cân bằng — chuẩn hiện nay).
  • Subword có 4 thuật toán chính: BPE, WordPiece, SentencePiece, Unigram (chi tiết Bài 3-4).
  • Token ≠ từ. Quy đổi tham khảo: 1000 token ≈ 750 từ tiếng Anh; tiếng Việt tốn ~2 lần token cho cùng nội dung.
  • Special token: <bos>, <eos>, <pad>, <unk> và các marker chat (<|im_start|>, <|eot_id|>) phụ thuộc model.
  • Context window và cost API đều tính theo token — luôn ước lượng qua tokenizer.encode(), không phải len(string).
  • Bug "đếm R trong strawberry" là tokenizer artifact, không phải lỗi reasoning. Cần thao tác ký tự → cho LLM gọi tool.