[01-whitespace-char-tokenizer] turns raw text into a list of string tokens, but [02-embeddings/01-token-embedding-lookup]'s embedding lookup, and everything downstream of it, needs INTEGER ids, not strings, a lookup table is indexed by position, not by spelling. Building that string-to-integer mapping (the "vocabulary") from a training corpus is this question's first job. The second, equally important job: a vocabulary built from a FIXED training corpus will, by definition, never contain every string that might appear in future text the model encounters, someone will eventually type a typo, a brand-new word, or a name the vocabulary has never seen. Rather than crashing or silently corrupting the input, a real tokenizer reserves one special token, conventionally called <unk> ("unknown"), as the fallback id for anything outside the known vocabulary.
A secondary, practical concern this question also handles: including every single token that appears even ONCE in a large training corpus can bloat the vocabulary with rare misspellings, one-off names, and noise, each of which would get its own learned embedding vector that barely ever gets a useful training signal. Filtering by a minimum frequency threshold (min_freq) keeps the vocabulary focused on tokens common enough to be worth a dedicated embedding.
Implement build_vocabulary(token_lists, min_freq, unk_token), counting every token's frequency across the whole corpus (a list of token lists), reserving id 0 for unk_token, and assigning ids to every OTHER token with frequency >= min_freq, in order of descending frequency (ties broken alphabetically, for a deterministic, reproducible result). Implement encode_with_unk(tokens, vocab, unk_token), mapping a list of tokens to their ids, substituting unk_token's id for anything not present in vocab.
unk_token must always be assigned id 0, regardless of whether it happens to also appear literally in the corpus.min_freq must be EXCLUDED from the vocabulary entirely (they'll map to <unk> at encoding time, via encode_with_unk).encode_with_unk must never raise a KeyError: any out-of-vocabulary token maps to unk_token's id instead.collections.Counter() with .update(tokens) called once per sequence in token_lists accumulates a total frequency count across the whole corpus in a few lines.
sorted(counts.items(), key=lambda kv: (-kv[1], kv[0])) sorts by frequency descending (-kv[1], negating to reverse the usual ascending sort) with alphabetical order (kv[0]) as the tiebreak, a single sort key handles both requirements at once.
Start vocab = {unk_token: 0}, then loop over the sorted (token, count) pairs, and for each one where count >= min_freq, assign vocab[token] = len(vocab) (the CURRENT size of vocab is exactly the next available id, since ids are assigned in order starting from 0).
Click "Run Tests" to test your implementation