class WordTokenizer(tk.Tokenizer):def__init__(self, vocab: dict[str, int], unk_token: str='<unk>'):super().__init__(vocab, unk_token=unk_token)@override@classmethoddef from_text(cls, text: str|list[str], unk_token: str='<unk>') -> Self:ifisinstance(text, str): text = [text] vocab_tokens = {word for line in text for word in line.split()} vocab_tokens = [unk_token] +sorted(vocab_tokens - {unk_token}) vocab = {token: idx for idx, token inenumerate(vocab_tokens)}return cls(vocab, unk_token)@overridedef encode(self, text: str) ->list[int]:return [self.token_to_id.get(word, self.unk_id) for word in text.split()]@overridedef decode(self, ids: list[int], skip_special_tokens: bool=True) ->str:if skip_special_tokens: special_tokens =set(self.special_tokens)else: special_tokens =set() tokens = []for i in ids: token =self.id_to_token[int(i)]if token notin special_tokens: tokens.append(token)return' '.join(tokens)
text ='deep learning is fun deep learning is useful'tokenizer = WordTokenizer.from_text(text)print('Vocab:', tokenizer.token_to_id)print('Encode known words:', tokenizer.encode('deep learning is fun'))print('Encode unknown word:', tokenizer.encode('MiniGPT is fun'))
words ='low lower lowest low lower newest'.split()words_freq = Counter(words)corpus = {word2symbols(w): f for w, f in words_freq.items()}merges = train_bpe(corpus, num_merges=10)print('Learned merges:')for i, pair inenumerate(merges, start=1):print(f'{i:2d}. {pair} -> {pair[0] + pair[1]}')
words = ['low', 'lower', 'lowest', 'newest', 'newer']n =max(len(word) for word in words)for word in words:print(f'{word:<{n}} -> {merge_word(word, merges)}')
\[
\text{logits} \in \mathbb{R}^{B \times T \times V}
\]
每一个 logit 都对应一个可能的下一个 token。
下面基于 BPE 的结果构造一个小词表。
def build_bpe_vocab( alphabet: set[str], merges: list[Pair], special_tokens: list[str],) ->dict[str, int]: tokens =set(alphabet) tokens.update(a + b for a, b in merges) vocab_tokens = special_tokens +sorted(tokens -set(special_tokens))return {token: i for i, token inenumerate(vocab_tokens)}alphabet = {sym for word in words for sym in word2symbols(word)}special_tokens = ['<pad>', '<bos>', '<eos>', '<unk>']vocab = build_bpe_vocab(alphabet, merges, special_tokens)id_to_token = {i: token for token, i in vocab.items()}print(vocab)print('Vocab size:', len(vocab))
不过需要注意,不同 GPT tokenizer 对 special tokens 的设计不完全一样。有些 decoder-only LM 会把 <eos> 同时用作 padding token,有些则会单独定义 pad token。这里先理解概念即可。
18.3.4.4 一个最小 BPE Tokenizer
现在我们把前面的函数包装成一个最小可用的 BPE tokenizer。
它仍然是教学版:
以空格切分单词;
在单词内部做 toy BPE;
用 </w> 表示词尾;
不处理真实 GPT tokenizer 里的 byte-level 细节。
class BPETokenizer(tk.Tokenizer):def__init__(self, vocab: dict[str, int], merges: list[Pair], unk_token: str='<unk>', ):self.merges = mergessuper().__init__(vocab, unk_token=unk_token)@override@classmethoddef from_text( cls, text: str|list[str], vocab_size: int=100, min_frequency: int=2, unk_token: str='<unk>', ) -> Self:ifisinstance(text, str): text = [text] word_freqs = Counter(word for line in text for word in line.split()) corpus = {word2symbols(w): f for w, f in word_freqs.items()} alphabet = {sym for symbols in corpus for sym in symbols} num_merges =max(0, vocab_size -len(alphabet) -1) merges = train_bpe(corpus, num_merges, min_frequency) vocab = build_bpe_vocab(alphabet, merges, [unk_token])return cls(vocab, merges, unk_token)@overridedef encode(self, text: str) ->list[int]: unk_id =self.vocab[self.unk_token] ids = []for word in text.split():for piece in merge_word(word, self.merges): ids.append(self.vocab.get(piece, unk_id))return ids@overridedef decode(self, ids: list[int], skip_special_tokens: bool=True) ->str:if skip_special_tokens: special_tokens =set(self.special_tokens)else: special_tokens =set() tokens = []for i in ids:ifself.id_to_token[int(i)] notin special_tokens: tokens.append(self.id_to_token[int(i)])return''.join(tokens).replace('</w>', ' ').strip()
测试一下:
text ='low lower lowest low lower newest deep learning deep learner'tokenizer = BPETokenizer.from_text(text, vocab_size=20)sample ='lower newest learner'ids = tokenizer.encode(sample)decoded = tokenizer.decode(ids)print('Sample:', sample)print('Ids:', ids)print('Tokens:', tokenizer.lookup_tokens(ids))print('Decoded:', decoded)print('Vocab size:', tokenizer.vocab_size)
text ='Here is a simple example of tokenization.'char_tokenizer = CharacterTokenizer.from_text(text)word_tokenizer = WordTokenizer.from_text(text)bpe_tokenizer = BPETokenizer.from_text(text, vocab_size=20)
1. 影响序列长度
同一段文本,用不同 tokenizer 编码后,token 数可能差很多。
text ="I don't like tokenization."char_ids = char_tokenizer.encode(text)word_ids = word_tokenizer.encode(text)bpe_ids = bpe_tokenizer.encode(text)print('Character tokens:', len(char_ids))print('Word tokens:', len(word_ids))print('BPE tokens:', len(bpe_ids))
Character tokens: 26
Word tokens: 4
BPE tokens: 26
text ="""Machine learning models learn patterns from data. A language model reads a sequenceof tokens and predicts what token is likely to appear next. During training, the modelgradually improves its predictions by comparing them with the correct answers.Tokenization is an important step because it determines how raw text is divided intosmaller units. Some tokenizers use characters, some use complete words, and modernlanguage models often use subword tokens to balance vocabulary size and sequence length."""text = text.replace('\n', ' ').strip()tokenizer = BPETokenizer.from_text(text, vocab_size=100)token_ids = tokenizer.encode(text)print('Token ids:', token_ids[:10], '...')print('Num tokens:', len(token_ids))