- Published on
Day 12: NLP Fundamentals & Tokenizer
- Authors

- Name
- Trần Mạnh Thắng
- @TranManhThang96
Mục tiêu
Sau bài này, bạn cần làm được các việc sau:
- Giải thích được pipeline
raw text -> normalized text -> tokens -> token ids -> tensors -> model. - Biết nên preprocessing text ở mức nào, không làm mất tín hiệu quan trọng của tiếng Việt.
- Phân biệt được BPE, WordPiece, SentencePiece/Unigram ở mức ứng dụng.
- Hiểu vocabulary, special tokens, OOV/UNK, padding, truncation, attention mask và offset mapping.
- Ước lượng được token limit, latency và cost/request trước khi đưa NLP/LLM vào production.
- Nhận diện được vì sao tiếng Việt có dấu, không dấu, tách âm tiết bằng khoảng trắng và word segmentation làm tokenization khó hơn English.
- Dùng được tokenizer wrapper có config, batch tokenization, token stats, cost estimator và warning khi input quá dài.
Liên kết với các ngày trước và sau
Day 11 nói về training loop: model chỉ nhận tensor, loss chỉ tính trên tensor, optimizer chỉ update weights. Day 12 trả lời câu hỏi: text thật của user biến thành tensor như thế nào?
Day 13 nói về attention: attention làm việc trên sequence token embedding và cần mask đúng. Nếu tokenizer, padding hoặc truncation sai, attention phía sau cũng sai.
Day 16 sẽ fine-tune BERT/PhoBERT classifier. Nếu train và inference dùng khác tokenizer hoặc khác preprocessing, model có thể giảm chất lượng rất mạnh dù training loop không báo lỗi.
TL;DR
Tokenizer là contract giữa raw text và model weights. Cùng một câu nhưng dùng tokenizer khác sẽ ra token ids khác, nghĩa là model đang nhìn một input khác. Với LLM/RAG, token không chỉ là chi tiết kỹ thuật; token quyết định context budget, latency, memory, throughput và tiền.
Trong production, không được xem tokenizer là helper phụ. Nó là một phần của model artifact, phải được version cùng model, được test bằng dữ liệu thật và được monitor bằng token length distribution, truncation rate, OOV/UNK rate nếu có.
1. NLP pipeline nhìn từ góc Senior SE
Pipeline tối giản:
raw text
-> input validation
-> text preprocessing / normalization
-> tokenization
-> token ids + attention mask
-> embedding lookup
-> model forward
-> output decoding / post-processing
Map sang backend:
| NLP concept | SE analogy |
|---|---|
| Raw text | HTTP request body chưa validate |
| Preprocessing | Input validation + canonicalization |
| Tokenizer | Parser + schema encoder |
| Vocabulary | Dictionary/index mapping stable |
| Token id | Internal enum/id mà model đã học |
| Attention mask | Mask/filter để bỏ qua padding |
| Truncation | Data loss policy |
| Context window | Request payload limit |
| Token cost | Billing unit + compute unit |
Điểm quan trọng: model không hiểu string trực tiếp. Model học embedding cho từng token id trong vocabulary. Nếu token id thay đổi, embedding lookup thay đổi; nếu preprocessing khác nhau giữa train và inference, đó là train-serving skew.
2. Text preprocessing: làm đủ, đừng làm quá tay
Preprocessing là bước biến raw text thành input ổn định hơn. Nhưng với Transformer/LLM, nhiều kỹ thuật "clean text" kiểu truyền thống có thể làm mất thông tin.
Step 1: xác định mục tiêu
Trước khi clean text, hỏi:
- Đây là text classification, search, RAG, summarization hay generation?
- Model là traditional ML, BERT-style encoder, PhoBERT, hay GPT-style LLM?
- Có cần giữ case, punctuation, URL, code, số hợp đồng, emoji, markdown không?
- Có được phép log raw text không, hay cần redaction PII?
- Input user có tiếng Việt không dấu, có dấu, code-mixing Việt/English không?
Không có preprocessing tốt nhất cho mọi bài toán.
Step 2: những xử lý thường nên làm
Các bước sau thường an toàn nếu được test:
- Validate input type, size và encoding.
- Normalize Unicode về
NFCđể giảm khác biệt biểu diễn ký tự có dấu. - Normalize whitespace: chuyển nhiều khoảng trắng, tab, newline không cần thiết về một format nhất quán.
- Loại bỏ control characters vô hình như zero-width space nếu chúng không có ý nghĩa nghiệp vụ.
- Parse HTML/Markdown bằng parser nếu nguồn là HTML/Markdown thật.
- Chuẩn hóa line ending nếu pipeline dùng text document.
- Redact PII trong log/debug output, không nhất thiết trong input đưa vào model.
Step 3: những xử lý phải cân nhắc kỹ
| Xử lý | Khi có thể dùng | Rủi ro |
|---|---|---|
| Lowercase | Baseline truyền thống, model uncased | Mất entity, mã lỗi, SKU, tên riêng |
| Xóa dấu tiếng Việt | Matching thô hoặc search tolerant | Mất nghĩa: ma, má, mà, mã, mạ |
| Xóa punctuation | TF-IDF/simple text cleanup | Mất intent, code, URL, markdown, câu hỏi |
| Xóa stopwords | Bag-of-words truyền thống | Thường không cần với Transformer/LLM |
| Stemming/lemmatization | Một số pipeline cổ điển | Tiếng Việt và code-mixing dễ sai |
| Dịch tiếng Việt sang English | Dùng model chỉ mạnh English | Thêm latency, cost, lỗi dịch, mất sắc thái |
Với BERT/PhoBERT/LLM, nguyên tắc mặc định là normalize nhẹ, giữ thông tin, rồi để tokenizer/model xử lý. Chỉ xóa hoặc biến đổi mạnh khi bạn có benchmark chứng minh tốt hơn.
3. Tokenization là gì?
Tokenization biến text thành chuỗi token. Token có thể là:
- Word:
xin,chào. - Subword:
token,##izer,ngôn,_ngữ. - Character hoặc byte.
- Special token:
[CLS],[SEP],[PAD],[UNK],<s>,</s>,<mask>.
Pipeline:
"Tôi đang học NLP"
-> ["Tôi", "đang", "học", "NL", "##P"]
-> [101, 1945, 1234, 5678, 9101, 102]
-> embedding vectors
Các token string chỉ để người đọc debug. Model thật dùng token ids.
4. Vocabulary, token ids và special tokens
Vocabulary là mapping ổn định từ token sang id:
"[PAD]" -> 0
"[UNK]" -> 100
"[CLS]" -> 101
"xin" -> 12345
Embedding matrix của model có một row cho mỗi token id. Vì vậy:
- Đổi tokenizer là breaking change với model.
- Đổi thứ tự vocabulary là breaking change.
- Thêm token mới cần resize embedding và fine-tune/retrain phù hợp.
- Cùng một text, token ids phải giống giữa training, evaluation và inference.
Special tokens thường gặp:
| Token | Vai trò |
|---|---|
[PAD] | Pad batch về cùng chiều dài |
[UNK] | Token unknown nếu tokenizer không encode được |
[CLS] | Đại diện sequence trong BERT classifier |
[SEP] | Ngăn cách sentence/pair input trong BERT |
[MASK] | Masked language modeling |
<s>, </s> | Begin/end sequence trong một số model |
<bos>, <eos> | Begin/end of sequence trong nhiều LLM |
5. OOV và UNK
OOV là out-of-vocabulary: text chứa từ hoặc ký tự tokenizer không có trong vocabulary.
Word-level tokenizer dễ OOV:
"tokenizerization" -> [UNK]
Subword tokenizer giảm OOV:
"tokenizerization" -> ["token", "##izer", "##ization"]
Byte-level tokenizer còn giảm OOV hơn nữa vì gần như mọi text có thể biểu diễn bằng byte. Nhưng "không bị UNK" không có nghĩa model hiểu tốt. Model có thể encode được mã lỗi lạ, tên sản phẩm mới hoặc tiếng Việt không dấu, nhưng semantic quality vẫn phụ thuộc dữ liệu pretraining/fine-tuning.
Production signal:
- UNK rate tăng sau release có thể báo domain drift.
- Nhiều token rất nhỏ cho cùng một từ có thể làm token count tăng, latency tăng và quality giảm.
- Không nên chỉ kiểm tra câu English sạch; cần sample tiếng Việt thật từ production.
6. BPE, WordPiece và SentencePiece
BPE
BPE là subword algorithm bắt đầu từ vocabulary nhỏ như character/byte, rồi học merge rules bằng cách ghép các cặp token xuất hiện thường xuyên.
Ví dụ trực giác:
["t", "o", "k", "e", "n"]
-> merge "t" + "o" = "to"
-> merge "to" + "k" = "tok"
-> ...
Khi dùng:
- GPT-style tokenizer thường dùng byte-level BPE.
- RoBERTa-style tokenizer dùng BPE.
- PhoBERT là RoBERTa-style cho tiếng Việt và thường cần đúng preprocessing/word segmentation theo model.
Trade-off:
- Ít OOV, hợp open vocabulary.
- Token preview có thể khó đọc.
- Token count khó đoán bằng word count, nhất là với tiếng Việt có dấu hoặc text code-mixed.
WordPiece
WordPiece phổ biến trong BERT. Nó học subword theo scoring khác BPE, và khi encode thường chọn subword dài nhất có trong vocabulary từ trái sang phải. Continuation token hay có prefix ##.
Ví dụ trực giác:
"tokenizer" -> ["token", "##izer"]
Khi dùng:
- BERT và nhiều BERT-style encoder.
- Text classification, NER, reranking, embedding model kiểu encoder.
Trade-off:
- Dễ debug hơn byte-level BPE trong nhiều trường hợp.
- Có thể sinh
[UNK]nếu gặp input quá lạ tùy tokenizer. - Với tiếng Việt, từ có dấu, không dấu và cách viết khác nhau có thể tách rất khác.
SentencePiece và Unigram
SentencePiece là framework/tokenizer library thường train trực tiếp trên raw text và xem whitespace như một symbol, ví dụ token có ký hiệu ▁ để đánh dấu đầu từ. SentencePiece có thể dùng Unigram hoặc BPE bên dưới.
Unigram thường bắt đầu từ vocabulary lớn, rồi loại dần token để tối ưu loss trên corpus. Khi encode, nó chọn segmentation có xác suất tốt nhất theo model.
Khi dùng:
- Nhiều multilingual model, T5-style model và một số LLaMA-style tokenizer.
- Bài toán có nhiều ngôn ngữ, không muốn phụ thuộc pre-tokenizer theo whitespace truyền thống.
Trade-off:
- Hợp multilingual và raw text.
- Token nhìn lạ hơn với người debug.
- Vẫn phải dùng đúng tokenizer đi kèm model.
7. Padding, truncation, attention mask và offsets
Model training/inference theo batch cần tensor cùng shape. Text thì dài ngắn khác nhau, nên cần padding:
input_ids:
[101, 10, 11, 102, 0, 0]
[101, 20, 21, 22, 23, 102]
attention_mask:
[ 1, 1, 1, 1, 0, 0]
[ 1, 1, 1, 1, 1, 1]
attention_mask = 0 cho model biết vị trí đó là padding, không phải nội dung thật.
Truncation là cắt input nếu vượt max_length. Đây là nơi dễ có bug production:
- Ticket dài bị cắt mất error code ở cuối.
- Contract bị cắt mất điều khoản quan trọng.
- Chat history bị cắt mất system instruction.
- RAG context bị cắt mất citation/source.
Các policy thường dùng:
| Policy | Khi dùng | Rủi ro |
|---|---|---|
| Reject | API cần độ chính xác cao, input vượt budget là lỗi rõ ràng | User phải sửa input hoặc hệ thống phải hướng dẫn |
| Truncate tail | Thông tin quan trọng thường ở đầu | Mất kết luận/cuối tài liệu |
| Truncate head | Chat history, log tail quan trọng hơn | Mất instruction hoặc background |
| Sliding window | Document dài, cần xử lý từng cửa sổ | Tăng compute và cần merge result |
| Summarize/compress | LLM/RAG input dài | Thêm model call, có thể mất chi tiết |
Offset mapping ánh xạ token về vị trí ký tự trong text gốc. Nó hữu ích cho NER, highlight citation, debug chunking. Với Hugging Face, return_offsets_mapping chỉ hoạt động với fast tokenizer.
8. Token limit, latency và cost
Token budget trong một LLM request:
total_tokens =
system_prompt_tokens
+ user_message_tokens
+ chat_history_tokens
+ retrieved_context_tokens
+ tool_schema_tokens
+ reserved_output_tokens
+ special_tokens_overhead
Nếu context window là 8,192 tokens và bạn reserve 1,000 output tokens, input budget thực tế không phải 8,192 mà khoảng 7,192 trừ overhead.
Cost estimate:
cost_usd =
input_tokens / 1_000_000 * input_price_per_1m
+ output_tokens / 1_000_000 * output_price_per_1m
Latency và memory:
- Tokenization tốn CPU, đặc biệt khi QPS cao hoặc document dài.
- Model inference tăng theo input length; Day 13 sẽ giải thích self-attention có chi phí gần
O(n^2)theo sequence length cho nhiều kiến trúc. - Decoder-only LLM còn tốn KV cache theo số token đã xử lý.
- Padding quá dài làm lãng phí compute; dynamic padding theo batch thường tốt hơn padding toàn bộ về max length cố định.
Production metrics nên log:
input_tokens,output_tokens,total_tokens.p50/p95/p99 input_tokens.truncation_count,truncation_rate.rejected_too_long_count.cost_per_request,cost_per_tenant,cost_per_feature.tokenization_latency_ms,model_latency_ms.
Không log raw text nếu có PII hoặc dữ liệu khách hàng nhạy cảm.
9. Tiếng Việt bị tokenize như thế nào?
Tiếng Việt có vài điểm khác English:
Khoảng trắng thường tách âm tiết, không chắc tách từ
Trong English, whitespace thường gần với word boundary:
"natural language processing" -> 3 words
Trong tiếng Việt:
"xử lý ngôn ngữ tự nhiên"
Chuỗi này có nhiều âm tiết, nhưng concept có thể là:
"xử_lý" "ngôn_ngữ" "tự_nhiên"
Một số model tiếng Việt như PhoBERT được train với text đã word-segmented, thường nối các âm tiết trong cùng một từ bằng _. Nếu fine-tune hoặc inference PhoBERT cho classifier, bạn cần theo đúng hướng dẫn preprocessing/tokenizer của model đang dùng.
Có dấu và không dấu là input khác
"ma" "má" "mà" "mã" "mạ"
Các chuỗi này khác Unicode, khác token và khác nghĩa. Xóa dấu có thể giúp một số search tolerant, nhưng với classification/LLM thường làm mất semantic signal.
Code-mixing rất phổ biến
Ticket thật có thể như sau:
"Khách hàng báo lỗi thanh toán PAY_403 lúc 09:30, cần retry idempotent."
Tokenizer có thể tách PAY_403, số giờ, dấu _, English term và tiếng Việt thành nhiều token nhỏ. Nếu hệ thống support/RAG dùng nhiều mã lỗi, SKU, log line, bạn phải test token count trên dữ liệu thật.
Token count tiếng Việt có thể cao hơn bạn đoán
Không nên dùng character count hoặc word count để suy ra token count. Một đoạn 500 từ tiếng Việt có thể có token count khác nhiều so với 500 từ English, tùy tokenizer.
Best practice:
- Đếm token bằng đúng tokenizer của model sẽ dùng.
- Benchmark câu có dấu, không dấu, code-mixed, markdown, bảng, JSON, log line.
- Với RAG, chunk theo token và validate retrieval quality, không chunk cứng theo số ký tự.
10. Best solution theo context
| Context | Khuyến nghị | Lý do |
|---|---|---|
| Fine-tune BERT/PhoBERT classifier | Dùng đúng AutoTokenizer và preprocessing của model; max_length 128/256/512 sau khi đo p95 | Chất lượng phụ thuộc train/inference consistency |
| Vietnamese enterprise RAG | Chunk theo token của embedding/generator, giữ metadata, overlap vừa đủ, reserve output budget | Tránh overflow context và mất citation |
| LLM chatbot có chat history | Đếm token mỗi turn, trim/summarize history có policy, không cắt system prompt | Giữ instruction và kiểm soát cost |
| Search baseline nhanh | Có thể dùng lowercase/normalize nhẹ + TF-IDF/BM25 | Rẻ, explainable, đủ tốt cho baseline |
| NER/highlight citation | Dùng fast tokenizer và offset mapping | Cần map token về text gốc |
| High-QPS service | Batch tokenization, dynamic padding, cache tokenized static docs | Giảm CPU và waste compute |
11. Dùng được trong production không?
Có, tokenizer và preprocessing pipeline dùng được trong production, nhưng cần các điều kiện sau:
- Version tokenizer cùng model artifact, config, label mapping và preprocessing code.
- Train, validation, batch inference và online inference dùng cùng contract.
- Có hard limit cho input tokens và reserved output tokens.
- Không silent truncation; nếu truncate phải log và có policy rõ.
- Có token stats trên dữ liệu thật: p50/p95/p99, max, too-long rate.
- Có test cho tiếng Việt có dấu, không dấu, code-mixed, mã lỗi, markdown, JSON/log.
- Không log raw PII; chỉ log length, hash/request id hoặc sample đã redact.
- Có cost estimator theo model price hiện tại và budget theo tenant/feature.
- Có fallback: reject rõ ràng, ask user shorten, summarize/compress, hoặc split document.
Không nên đưa vào production nếu:
- Chưa biết tokenizer đang dùng có đúng với model không.
- Đang rely vào default truncation mà không log.
- Chunking dựa hoàn toàn vào character count.
- Chưa test tiếng Việt thật.
- Không có giới hạn cost/request.
12. Thứ tự học và thực hành
- Đọc lại mental model
raw text -> token ids -> embedding. - Đọc bảng BPE/WordPiece/SentencePiece và tự giải thích bằng lời của mình.
- Chạy code trong
document.mdvới 3 tokenizer:bert-base-multilingual-cased,gpt2,vinai/phobert-base. - Làm bài tập token stats trong
exercise.md. - Ghi lại quyết định tokenizer/preprocessing để dùng lại ở Day 16.
Tự kiểm tra nhanh
- Vì sao tokenizer phải version cùng model weights?
- Vì sao xóa dấu tiếng Việt có thể làm model tệ hơn?
- Khi nào nên reject input quá dài thay vì truncate?
- Attention mask khác gì token type ids?
- Vì sao
return_offsets_mappingquan trọng cho NER/citation? - Nếu p95 input tokens vượt
max_length, bạn sẽ sửa dữ liệu, model, chunking hay policy?
Tài liệu
1. Pipeline tham chiếu
Client text
-> validate size/type
-> normalize Unicode + whitespace
-> optional redaction for logs
-> tokenizer(texts, padding, truncation, max_length)
-> input_ids + attention_mask + optional offsets
-> model
-> decode / classify / rank / generate
Trong production, tokenizer wrapper nên là một dependency rõ ràng, có config và test. Không nên rải AutoTokenizer.from_pretrained(...) khắp codebase.
2. API notes từ Hugging Face
Các điểm cần nhớ khi dùng Hugging Face:
AutoTokenizer.from_pretrained(model_name, use_fast=True)load tokenizer theo model repository.- Tokenizer có thể nhận một string hoặc một batch
list[str]. padding="longest"pad theo sequence dài nhất trong batch;padding="max_length"pad cố định theomax_length.truncation=Truehoặc strategy như"longest_first","only_first","only_second"sẽ cắt input vượtmax_length.return_attention_mask=Truetrả mask để model bỏ qua padding.return_offsets_mapping=Truetrả(char_start, char_end)cho token, nhưng chỉ hỗ trợ fast tokenizer.return_length=Truetrả length sau khi tokenization/padding/truncation.pad_to_multiple_of=8có thể hữu ích cho tensor cores/GPU khi có padding.- Với thư viện
tokenizers,encode_batchxử lý batch song song và trảtokens,ids,attention_mask,offsets,special_tokens_mask.
Context7 library IDs đã dùng khi viết bài:
/websites/huggingface_co_transformers_main/huggingface/tokenizers/huggingface/course
3. Preprocessing helper tối thiểu
Helper này cố tình normalize nhẹ. Nó không xóa dấu, không lowercase mặc định, không xóa punctuation.
from __future__ import annotations
import re
import unicodedata
ZERO_WIDTH_CHARS = "\u200b\u200c\u200d\ufeff"
def normalize_text(text: str, *, unicode_form: str = "NFC") -> str:
if not isinstance(text, str):
raise TypeError(f"text must be str, got {type(text)!r}")
normalized = unicodedata.normalize(unicode_form, text)
normalized = normalized.translate({ord(ch): None for ch in ZERO_WIDTH_CHARS})
normalized = re.sub(r"[ \t\r\f\v]+", " ", normalized)
normalized = re.sub(r"\n{3,}", "\n\n", normalized)
return normalized.strip()
Nếu cần parse HTML, dùng parser như BeautifulSoup/lxml ở ingestion layer. Không nên dùng regex tùy tiện cho HTML phức tạp.
4. Production-oriented tokenizer wrapper
Ví dụ dưới đây tập trung vào phần tokenizer, không gọi model. Nó phù hợp để dùng trong API service, batch inference hoặc ingestion pipeline sau khi bạn bổ sung test, metric exporter và model-specific preprocessing.
from __future__ import annotations
import logging
import re
import statistics
import time
import unicodedata
from dataclasses import dataclass
from typing import Any, Literal
from transformers import AutoTokenizer, PreTrainedTokenizerBase
logger = logging.getLogger("day12.tokenizer")
ZERO_WIDTH_CHARS = "\u200b\u200c\u200d\ufeff"
TruncationPolicy = Literal["reject", "truncate"]
PaddingPolicy = Literal[False, "longest", "max_length"]
class TokenBudgetError(ValueError):
def __init__(self, *, max_length: int, too_long_count: int, max_seen: int) -> None:
super().__init__(
f"input exceeds token budget: max_length={max_length}, "
f"too_long_count={too_long_count}, max_seen={max_seen}"
)
self.max_length = max_length
self.too_long_count = too_long_count
self.max_seen = max_seen
@dataclass(frozen=True)
class TokenizerConfig:
model_name: str
max_length: int
truncation_policy: TruncationPolicy = "reject"
truncation_strategy: Literal["longest_first", "only_first", "only_second"] = "longest_first"
padding: PaddingPolicy = "longest"
pad_to_multiple_of: int | None = 8
add_special_tokens: bool = True
return_offsets: bool = False
use_fast: bool = True
warning_ratio: float = 0.9
allow_eos_as_pad: bool = False
@dataclass(frozen=True)
class PriceConfig:
input_usd_per_1m_tokens: float
output_usd_per_1m_tokens: float
@dataclass(frozen=True)
class TokenStats:
count: int
min_tokens: int
max_tokens: int
mean_tokens: float
p50_tokens: int
p95_tokens: int
p99_tokens: int
too_long_count: int
@dataclass(frozen=True)
class BatchTokenizationResult:
encoded: dict[str, Any]
raw_token_lengths: list[int]
stats: TokenStats
tokenization_latency_ms: float
def normalize_text(text: str, *, unicode_form: str = "NFC") -> str:
if not isinstance(text, str):
raise TypeError(f"text must be str, got {type(text)!r}")
normalized = unicodedata.normalize(unicode_form, text)
normalized = normalized.translate({ord(ch): None for ch in ZERO_WIDTH_CHARS})
normalized = re.sub(r"[ \t\r\f\v]+", " ", normalized)
normalized = re.sub(r"\n{3,}", "\n\n", normalized)
return normalized.strip()
def percentile(values: list[int], pct: float) -> int:
if not values:
return 0
ordered = sorted(values)
idx = round((len(ordered) - 1) * pct)
return ordered[idx]
def build_stats(lengths: list[int], *, max_length: int) -> TokenStats:
if not lengths:
return TokenStats(0, 0, 0, 0.0, 0, 0, 0, 0)
too_long = sum(length > max_length for length in lengths)
return TokenStats(
count=len(lengths),
min_tokens=min(lengths),
max_tokens=max(lengths),
mean_tokens=statistics.fmean(lengths),
p50_tokens=percentile(lengths, 0.50),
p95_tokens=percentile(lengths, 0.95),
p99_tokens=percentile(lengths, 0.99),
too_long_count=too_long,
)
def estimate_cost_usd(
*,
input_tokens: int,
output_tokens: int,
price: PriceConfig,
requests: int = 1,
) -> float:
per_request = (
input_tokens / 1_000_000 * price.input_usd_per_1m_tokens
+ output_tokens / 1_000_000 * price.output_usd_per_1m_tokens
)
return per_request * requests
class TokenizerService:
def __init__(self, config: TokenizerConfig) -> None:
self.config = config
self.tokenizer: PreTrainedTokenizerBase = AutoTokenizer.from_pretrained(
config.model_name,
use_fast=config.use_fast,
)
if config.return_offsets and not self.tokenizer.is_fast:
raise ValueError("return_offsets=True requires a fast tokenizer")
if self.tokenizer.pad_token is None:
if config.allow_eos_as_pad and self.tokenizer.eos_token is not None:
self.tokenizer.pad_token = self.tokenizer.eos_token
else:
raise ValueError(
f"{config.model_name} has no pad_token. Set a pad token explicitly "
"or enable allow_eos_as_pad for GPT-style inference-only batching."
)
@property
def vocab_size(self) -> int:
return int(self.tokenizer.vocab_size)
@property
def special_tokens(self) -> dict[str, str | list[str]]:
return dict(self.tokenizer.special_tokens_map)
def count_tokens(self, texts: list[str]) -> list[int]:
normalized = [normalize_text(text) for text in texts]
encoded = self.tokenizer(
normalized,
add_special_tokens=self.config.add_special_tokens,
padding=False,
truncation=False,
return_attention_mask=False,
verbose=False,
)
return [len(input_ids) for input_ids in encoded["input_ids"]]
def encode_batch(self, texts: list[str]) -> BatchTokenizationResult:
if not texts:
raise ValueError("texts must not be empty")
started = time.perf_counter()
normalized = [normalize_text(text) for text in texts]
raw_lengths = self.count_tokens(normalized)
stats = build_stats(raw_lengths, max_length=self.config.max_length)
warn_threshold = int(self.config.max_length * self.config.warning_ratio)
near_limit_count = sum(length >= warn_threshold for length in raw_lengths)
if near_limit_count:
logger.warning(
"token budget near limit: model=%s max_length=%s near_limit_count=%s "
"max_seen=%s p95=%s",
self.config.model_name,
self.config.max_length,
near_limit_count,
stats.max_tokens,
stats.p95_tokens,
)
if stats.too_long_count:
logger.warning(
"token budget exceeded: model=%s max_length=%s too_long_count=%s "
"max_seen=%s action=%s",
self.config.model_name,
self.config.max_length,
stats.too_long_count,
stats.max_tokens,
self.config.truncation_policy,
)
if self.config.truncation_policy == "reject":
raise TokenBudgetError(
max_length=self.config.max_length,
too_long_count=stats.too_long_count,
max_seen=stats.max_tokens,
)
should_truncate = self.config.truncation_policy == "truncate"
encoded = self.tokenizer(
normalized,
add_special_tokens=self.config.add_special_tokens,
padding=self.config.padding,
truncation=self.config.truncation_strategy if should_truncate else False,
max_length=self.config.max_length if should_truncate or self.config.padding == "max_length" else None,
pad_to_multiple_of=self.config.pad_to_multiple_of,
return_attention_mask=True,
return_offsets_mapping=self.config.return_offsets,
return_length=True,
)
latency_ms = (time.perf_counter() - started) * 1000
return BatchTokenizationResult(
encoded=dict(encoded),
raw_token_lengths=raw_lengths,
stats=stats,
tokenization_latency_ms=latency_ms,
)
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
samples = [
"Tôi đang học xử lý ngôn ngữ tự nhiên và tokenizer cho hệ thống RAG.",
"Toi dang hoc xu ly ngon ngu tu nhien va tokenizer cho he thong RAG.",
"Khách hàng báo lỗi thanh toán PAY_403 lúc 09:30, cần retry idempotent.",
"Tôi đang_học xử_lý ngôn_ngữ tự_nhiên với PhoBERT.",
]
service = TokenizerService(
TokenizerConfig(
model_name="bert-base-multilingual-cased",
max_length=64,
truncation_policy="reject",
padding="longest",
return_offsets=True,
)
)
result = service.encode_batch(samples)
print("model:", service.config.model_name)
print("vocab_size:", service.vocab_size)
print("special_tokens:", service.special_tokens)
print("raw_token_lengths:", result.raw_token_lengths)
print("stats:", result.stats)
print("latency_ms:", round(result.tokenization_latency_ms, 2))
first_ids = result.encoded["input_ids"][0]
print("first_tokens:", service.tokenizer.convert_ids_to_tokens(first_ids))
demo_price = PriceConfig(input_usd_per_1m_tokens=0.15, output_usd_per_1m_tokens=0.60)
print(
"estimated_monthly_cost_usd:",
round(
estimate_cost_usd(
input_tokens=result.stats.p95_tokens,
output_tokens=700,
price=demo_price,
requests=1_000_000,
),
2,
),
)
5. Cách chạy thử
Cài dependency:
pip install transformers tokenizers sentencepiece
Chạy với các tokenizer khác nhau bằng cách đổi model_name:
TokenizerConfig(model_name="bert-base-multilingual-cased", max_length=128)
TokenizerConfig(model_name="vinai/phobert-base", max_length=256)
TokenizerConfig(model_name="gpt2", max_length=256, allow_eos_as_pad=True)
Ghi chú:
gpt2là ví dụ byte-level BPE theo phong cách GPT cũ, hữu ích để quan sát tokenization, không đại diện cho toàn bộ tokenizer của các LLM mới.- Với PhoBERT, hãy kiểm tra model card/preprocessing chính thức. Nhiều workflow PhoBERT yêu cầu Vietnamese word segmentation trước khi tokenize.
- Với tokenizer không có
pad_token, không nên âm thầm dùngeos_tokenlàm pad trong mọi trường hợp. Chỉ bậtallow_eos_as_pad=Truekhi bạn hiểu tác động với model và task.
6. Công thức chọn max_length
Quy trình thực tế:
- Lấy sample production hoặc staging đủ đại diện.
- Normalize đúng như inference.
- Đếm token bằng đúng tokenizer của model.
- Xem p50/p95/p99 và max.
- Chọn
max_lengththeo SLA, VRAM/CPU, quality và cost. - Quyết định policy cho phần vượt budget: reject, truncate, sliding window hoặc summarize.
- Thêm metric và alert cho too-long rate.
Ví dụ:
p50 = 72 tokens
p95 = 238 tokens
p99 = 480 tokens
max = 3,900 tokens
Nếu classifier dùng BERT/PhoBERT:
max_length=256có thể cover khoảng 95%.- 5% còn lại cần policy rõ: reject, truncate hoặc split.
- Nếu 5% này là nhóm ticket VIP/complex case, truncate có thể làm giảm business quality.
Nếu RAG ingestion:
- Đừng dùng
max_length=256để cắt document gốc. - Split document thành chunk 300-800 tokens tùy embedding model và retrieval benchmark.
- Giữ overlap vừa đủ, ví dụ 50-120 tokens, nhưng phải evaluate.
7. Trade-off nhanh
| Quyết định | Lợi ích | Chi phí/rủi ro |
|---|---|---|
| Dynamic padding | Ít waste compute | Shape thay đổi giữa batch |
Padding max_length | Shape ổn định | Lãng phí nếu text ngắn |
| Reject quá dài | Không mất dữ liệu âm thầm | User cần retry hoặc hệ thống cần hướng dẫn |
| Truncate | Dễ vận hành | Có thể mất thông tin quyết định |
| Sliding window | Xử lý document dài tốt hơn | Tăng compute, cần aggregate output |
| Cache tokenized chunks | Tăng throughput ingestion/RAG | Cache invalidation khi tokenizer đổi |
| Offset mapping | Debug/citation/NER tốt | Chỉ fast tokenizer, thêm payload |
| Xóa dấu | Có thể tăng recall search thô | Giảm semantic quality cho NLP/LLM |
8. Production checklist riêng cho tokenizer
- Tokenizer, model weights, config, preprocessing code và label mapping cùng version.
- Unit test cho token count và special tokens với sample cố định.
- Contract test: batch size 1 và batch size N cho output tương thích.
- Test tiếng Việt có dấu, không dấu, word-segmented, code-mixed, markdown, JSON/log.
- Log token length, truncation, rejection, latency và cost; không log raw PII.
- Alert khi p95 token tăng bất thường hoặc rejection rate tăng.
- Benchmark dynamic padding vs max padding với QPS thật.
- Document rõ policy khi input vượt budget.
Bài tập
Mục tiêu thực hành
Sau bài tập này, bạn phải có một notebook hoặc script nhỏ trả lời được:
- Tokenizer nào tạo bao nhiêu token cho tiếng Việt có dấu, không dấu và code-mixed?
- p95 token count của sample domain của bạn là bao nhiêu?
max_lengthnên đặt thế nào cho classifier hoặc RAG chunk?- Request vượt token budget sẽ bị reject, truncate hay split?
- Cost ước lượng trên 1,000,000 requests là bao nhiêu?
Chuẩn bị
Cài dependency:
pip install transformers tokenizers sentencepiece
Tạo file script hoặc notebook trong môi trường học của bạn. Nếu tạo script minh họa trong repo này, đặt trong folder:
lessions/day-12-nlp-fundamentals-tokenizer/
Không cần train model ở Day 12. Mục tiêu là hiểu và kiểm soát tokenizer.
Bài 1: So sánh tokenizer
Dùng các model/tokenizer sau:
bert-base-multilingual-cased: WordPiece, BERT-style.gpt2: byte-level BPE, GPT-style cũ để quan sát hành vi byte/subword.vinai/phobert-base: Vietnamese RoBERTa/PhoBERT-style.
Input mẫu:
texts = {
"vi_with_diacritics": "Tôi đang học xử lý ngôn ngữ tự nhiên và tokenizer cho hệ thống RAG.",
"vi_no_diacritics": "Toi dang hoc xu ly ngon ngu tu nhien va tokenizer cho he thong RAG.",
"support_ticket": "Khách hàng báo lỗi thanh toán PAY_403 lúc 09:30, cần retry idempotent.",
"markdown": "## Lỗi thanh toán\n- Mã lỗi: PAY_403\n- User nói: không thanh toán được.",
"phobert_segmented": "Tôi đang_học xử_lý ngôn_ngữ tự_nhiên với PhoBERT.",
}
Yêu cầu:
- In
tokens,input_ids,attention_maskcho từng text. - In
len(input_ids)trước padding. - Với fast tokenizer, in
offset_mappingcho ít nhất một câu. - Ghi nhận tokenizer nào tách tiếng Việt thành nhiều token hơn.
- Ghi nhận khác biệt giữa có dấu và không dấu.
Câu hỏi tự trả lời:
- Token preview của WordPiece khác byte-level BPE như thế nào?
- Có tokenizer nào không có
pad_tokenkhông? Bạn xử lý ra sao? - PhoBERT khác gì khi input đã word-segmented bằng
_?
Bài 2: Token stats cho domain thật
Tạo ít nhất 30 câu/ticket/document snippet tiếng Việt từ domain bạn quan tâm. Ví dụ:
- Support ticket.
- Log lỗi thanh toán.
- FAQ nội bộ.
- Mô tả sản phẩm.
- Đoạn hợp đồng.
- Comment user có tiếng Việt không dấu.
Yêu cầu:
- Normalize text bằng helper trong
document.md. - Đếm token bằng đúng tokenizer bạn định dùng.
- Tính
min,mean,p50,p95,p99,max. - In 5 sample dài nhất cùng token count, nhưng redact PII nếu có.
- Đề xuất
max_lengthcho classifier.
Gợi ý quyết định:
| Kết quả đo | Quyết định khả thi |
|---|---|
| p95 <= 128, max <= 256 | max_length=256 thường ổn cho classifier |
| p95 <= 256, p99 cao | max_length=256 + policy cho outlier |
| p95 > 512 | Cần split/summarize hoặc đổi task design |
| Có nhiều mã lỗi/SKU bị tách nhỏ | Cần test quality riêng cho nhóm token này |
Bài 3: Token budget và cost
Giả sử hệ thống RAG có:
context_window = 8192
reserved_output_tokens = 1000
system_prompt_tokens = 350
tool_schema_tokens = 500
chat_history_tokens = 1200
Yêu cầu:
- Tính budget còn lại cho retrieved context.
- Nếu mỗi chunk là 450 tokens, tính số chunk tối đa có thể nhét vào context.
- Nếu input price là
0.15 USD / 1M tokens, output price là0.60 USD / 1M tokens, ước lượng cost cho 1 request. - Ước lượng cost cho 1,000,000 requests/tháng với p95 input tokens của bạn.
- Đề xuất hard limit theo tenant hoặc feature.
Công thức:
available_context =
context_window
- reserved_output_tokens
- system_prompt_tokens
- tool_schema_tokens
- chat_history_tokens
cost_usd =
input_tokens / 1_000_000 * input_price
+ output_tokens / 1_000_000 * output_price
Bài 4: Implement policy khi input quá dài
Dùng TokenizerService trong document.md, viết 3 test case:
- Input ngắn hơn
max_length: encode thành công. - Input vượt
max_lengthvớitruncation_policy="reject": raiseTokenBudgetError. - Input vượt
max_lengthvớitruncation_policy="truncate": encode thành công nhưng có warning log.
Acceptance criteria:
- Không có silent truncation.
- Log có
model,max_length,too_long_count,max_seen,action. - Test không assert raw text trong log.
- Batch size 1 và batch size N đều chạy.
Bài 5: Mini design review
Viết một đoạn design note ngắn cho use case của bạn:
Use case:
Model/tokenizer:
Preprocessing:
max_length:
Reserved output tokens:
Truncation policy:
Fallback khi quá dài:
Metrics:
PII/logging policy:
Cost estimate:
Trả lời bắt buộc:
- Dùng được trong production không?
- Nếu có, cần điều kiện gì?
- Trade-off lớn nhất là gì?
- Bạn sẽ monitor metric nào đầu tiên sau release?
Quiz
- Vì sao đổi tokenizer có thể làm hỏng model dù code inference không lỗi?
- Vì sao tiếng Việt không nên chunk bằng số từ tách theo whitespace?
- Khi nào
padding="max_length"tốt hơnpadding="longest"? - OOV/UNK rate cao báo hiệu vấn đề gì?
- Vì sao
offset_mappingcần thiết cho NER hoặc citation highlight? - Silent truncation có thể gây bug nghiệp vụ nào trong RAG?
- Vì sao token count ảnh hưởng cả latency, memory và cost?
Kết quả mong đợi
Bạn hoàn thành Day 12 khi có:
- Bảng so sánh token count của BERT, GPT-style và PhoBERT trên text tiếng Việt.
- Thống kê p50/p95/p99 token count cho sample domain.
- Một quyết định
max_lengthcó lý do. - Một policy rõ cho input quá dài.
- Một ước lượng cost/request và cost/tháng.
- Một câu trả lời production readiness cụ thể, không chung chung.