How Does Tokenization Work for NLP Models: Inside the TinyTorch Implementation
Tokenization converts raw text into integer IDs that NLP models can process, using either character-level mapping or Byte-Pair Encoding (BPE) subword algorithms as implemented in the Harvard Edge TinyTorch educational library.
Tokenization serves as the critical first step in any natural language processing pipeline, transforming human-readable text into numerical representations that neural networks can understand. In the harvard-edge/cs249r_book repository, the TinyTorch library provides a pedagogical implementation of tokenization strategies used by modern large language models. This article examines how tokenization works for NLP models by analyzing the source code in tinytorch/src/10_tokenization/10_tokenization.py, covering both simple character-level approaches and sophisticated subword algorithms.
The Tokenizer Base Interface
Every tokenizer in the TinyTorch ecosystem inherits from the abstract Tokenizer base class defined in tinytorch/src/10_tokenization/10_tokenization.py#L308-L342. This design establishes a uniform contract across all tokenization strategies.
The base class defines two essential methods:
encode(text: str) -> List[int]: Converts raw text into a sequence of integer token IDsdecode(tokens: List[int]) -> str: Reconstructs the original text from token IDs
This abstraction allows downstream components—such as embedding layers and transformer blocks—to remain agnostic to the specific tokenization strategy employed. Whether using character-level or BPE tokenization, the interface remains consistent.
Character-Level Tokenization with CharTokenizer
The CharTokenizer class, implemented in tinytorch/src/10_tokenization/10_tokenization.py#L380-L460, provides the simplest form of tokenization for NLP models. It maps each Unicode character directly to a unique integer identifier.
Building the Vocabulary
The tokenizer constructs a bidirectional mapping between characters and IDs using char_to_id and id_to_char dictionaries. The vocabulary includes a special <UNK> token (assigned ID 0) to handle unknown characters that may appear during encoding.
Encoding and Decoding Process
During encoding, the tokenizer iterates through each character in the input text and looks up its corresponding ID. If a character is not present in the vocabulary, it falls back to the <UNK> token ID.
During decoding, the process reverses: token IDs map back to characters via id_to_char, with unknown IDs rendering as <UNK>.
This approach guarantees zero out-of-vocabulary errors since every Unicode code point can be represented, though it produces longer token sequences (one token per character).
from tinytorch.core.tokenization import CharTokenizer
vocab = ['h', 'e', 'l', 'o', ' ', 'w', 'r', 'd']
char_tok = CharTokenizer(vocab)
text = "hello world!"
ids = char_tok.encode(text) # → [1, 2, 3, 3, 4, 5, 0, 6, 4, 7, 3, 5, 0]
print("IDs:", ids)
recovered = char_tok.decode(ids) # → "hello world"
print("Recovered:", recovered)
Byte-Pair Encoding (BPE) Tokenization
Modern NLP models predominantly use Byte-Pair Encoding (BPE), a subword tokenization algorithm that balances vocabulary size against sequence length. The BPETokenizer class in tinytorch/src/10_tokenization/10_tokenization.py#L466-L740 implements this greedy merge strategy.
The BPE Training Algorithm
The training process follows these steps:
- Initialization: Each word splits into characters plus an end-of-word marker
</w>via_get_word_tokens. - Pair Counting: The
_count_byte_pairsmethod (lines 530-566) tallies frequency-weighted adjacent character pairs across the corpus. - Merge Selection: Identify the most frequent pair for merging.
- Merge Execution: The
_merge_pairmethod (lines 598-637) replaces every occurrence of the selected pair with a concatenated token across all word token lists. - Iteration: Repeat steps 2-4 until reaching the target
vocab_sizeor exhausting merge candidates. - Mapping Construction: Build
token_to_idandid_to_tokendictionaries via_build_mappings.
Encoding New Text
After training, encoding new sentences involves:
- Splitting text into words and converting to character tokens with
_get_word_tokens - Applying the learned merge sequence via
_apply_merges - Converting final tokens to integer IDs using
token_to_id
Decoding reverses this process, concatenating tokens, stripping </w> markers, and normalizing whitespace.
from tinytorch.core.tokenization import BPETokenizer
corpus = ["hello", "world", "hello", "hell"]
bpe = BPETokenizer(vocab_size=20)
bpe.train(corpus)
# Encode a new sentence
sentence = "hello world"
token_ids = bpe.encode(sentence)
print("BPE token IDs:", token_ids)
# Decode back to text
print("Decoded:", bpe.decode(token_ids))
Integrating Tokenization into the NLP Pipeline
In the TinyTorch architecture, the tokenization module sits at the head of the data flow:
Raw Text ──► Tokenizer (CharTokenizer / BPETokenizer) ──► Token IDs ──► Embedding Layer
The embedding layer expects integer token IDs and produces dense vector representations for each token. Downstream transformer and attention mechanisms operate on these embedded sequences.
Because all tokenizers implement the standard encode/decode interface, switching between character-level and BPE strategies requires only a single line change. This modularity enables rapid experimentation with different tokenization approaches without modifying model architecture.
Summary
- Tokenization transforms text into integer IDs, serving as the essential bridge between human language and neural network computation.
- The
Tokenizerbase class intinytorch/src/10_tokenization/10_tokenization.pydefines a consistentencode/decodeinterface used by all implementations. - CharTokenizer provides character-level tokenization that never produces out-of-vocabulary tokens but creates longer sequences.
- BPETokenizer implements the Byte-Pair Encoding algorithm through methods like
_count_byte_pairsand_merge_pair, learning subword units that balance vocabulary efficiency and expressiveness. - The modular design allows seamless integration with embedding layers and facilitates educational exploration of tokenization internals.
Frequently Asked Questions
What is the difference between character-level and BPE tokenization?
Character-level tokenization assigns a unique ID to every individual character, resulting in small vocabularies but long sequences. BPE tokenization learns to merge frequently co-occurring character pairs into subword units, creating compact vocabularies that handle rare words through decomposition while keeping sequences shorter.
How does BPE handle out-of-vocabulary words?
BPE handles unknown words by breaking them into known subword units learned during training. For example, if "tokenization" was not in the training data but the model learned "token" and "ization" as separate tokens, it can represent the new word as a combination of existing subwords, eliminating true out-of-vocabulary errors.
Why is tokenization necessary for NLP models?
Neural networks operate on numerical tensors, not raw text. Tokenization converts variable-length strings into fixed-vocabulary integer sequences that can be embedded into dense vectors and processed by mathematical operations in transformer or recurrent architectures.
How do you choose the vocabulary size for BPE?
The vocabulary size represents a trade-off between sequence length and model capacity. Smaller vocabularies require longer token sequences but have fewer embedding parameters, while larger vocabularies shorten sequences but increase memory requirements. The BPETokenizer accepts vocab_size as a constructor parameter, typically set between 10,000 and 50,000 tokens for general-purpose models.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →