# Tokenizer is splitting special token

**URL:** </static-proxy?url=https%3A%2F%2Fdiscuss.huggingface.co%2Ft%2Ftokenizer-is-splitting-special-token%2F161147%3E%5C
**Category:** 🤗Tokenizers\
**Created:** [June 30, 2025, 2:43am UTC](/static-proxy?url=https%3A%2F%2Fdiscuss.huggingface.co%2Ft%2Ftokenizer-is-splitting-special-token%2F161147 "2025-06-30T02:43:18Z")\
**Posts on this page:** 4\
**Page:** 1

<div class="post-metadata">

**Author:** ![torch-nn](https://avatars.discourse-cdn.com/v4/letter/t/e9bcb4/32.png) [@torch-nn](/static-proxy?url=https%3A%2F%2Fdiscuss.huggingface.co%2Fu%2Ftorch-nn)%5C
**Post date:** [June 30, 2025, 2:43am UTC](/static-proxy?url=https%3A%2F%2Fdiscuss.huggingface.co%2Ft%2Ftokenizer-is-splitting-special-token%2F161147%2F1 "2025-06-30T02:43:18Z")

</div>

I’m trying to recreate the BERTweet tokenizers for my own use using the tokenizers library. Here is my code so far (training data was obtained from the Kaggle “Natural Language Processing with Disaster Tweets” Competition):

```auto
import tokenizers
from tokenizers import normalizers, pre_tokenizers, processors, trainers, Tokenizer, models
from tokenizers.trainers import WordPieceTrainer
from tokenizers.normalizers import NFD, NFC, StripAccents, BertNormalizer, Replace, Lowercase
from tokenizers.pre_tokenizers import Whitespace, Punctuation, Split
from transformers import AutoTokenizer
from tokenizers.processors import TemplateProcessing

tokenizer_df = pd.concat([full_df, test_df], axis=0) # concatting all data points

tokenizer = Tokenizer(
    models.WordPiece(unk_token='[UNK]')
)

# Normalization
tokenizer.normalizer = normalizers.Sequence([
    Replace(tokenizers.Regex(r"http\S+|www\.\S+"), "HTTPURL"),
    Replace(tokenizers.Regex(r"@\w+"), "@USER"), # From the BERTweet Paper
    NFD(),
    StripAccents(),
    Replace(r"\s+", " "), # Collapsing whitespace
])

# Pre-tokenization (provides the “legal cut points” that the sub-word encoder may merge inside, but never across)
tokenizer.pre_tokenizer = pre_tokenizers.Sequence([
    Split(tokenizers.Regex(r'HTTPURL'), behavior="isolated"), # makes HTTPURL and @USER are standalone tokens (cannot be split)
    Split(tokenizers.Regex(r'@USER'), behavior='isolated'),
    Split(tokenizers.Regex(r"#\w+"), behavior="isolated"), # isolate by hashtags
    Punctuation("isolated"), # isolate by punctuation marks
    Whitespace(), # Split based on spaces
])

special_tokens = ["[CLS]", "[PAD]", "[SEP]", "[MASK]", "[UNK]", 'HTTPURL', '@USER']
trainer = WordPieceTrainer(
    vocab_size=8000,
    special_tokens=special_tokens,
)

# Model Training
tokenizer.train_from_iterator(tokenizer_df['text'], trainer=trainer) # 10K training points

cls_token_id = tokenizer.token_to_id('[CLS]')
sep_token_id = tokenizer.token_to_id('[SEP]')

# Post-processing
tokenizer.post_processor = TemplateProcessing(
    single="[CLS] $A [SEP]",
    pair="[CLS] $A [SEP] $B:1 [SEP]:1",
    special_tokens=[("[CLS]", cls_token_id), ("[SEP]", sep_token_id)],
)

```

After training, I tested it on a bit of text:

```auto
from transformers import PreTrainedTokenizerFast

wrapped_tokenizer = PreTrainedTokenizerFast(
    tokenizer_object=tokenizer, # <- your trained WordPiece, untouched
    unk_token="[UNK]",
    cls_token="[CLS]",
    sep_token="[SEP]",
    pad_token="[PAD]",
    mask_token="[MASK]",
    additional_special_tokens=['HTTPURL', '@USER'],
)
text_input = "@RockyFire Update => California Hwy. 20 closed in both directions due to Lake County fire - #CAfire"
encoded_input = wrapped_tokenizer(text_input)
print(encoded_input)
print(wrapped_tokenizer.convert_ids_to_tokens(encoded_input['input_ids']))

```

The following is the output from the last print statement:

```auto
['[CLS]', '@', 'USER', 'Update', '=', '>', 'California', 'Hwy', '.', '20', 'closed', 'in', 'both', 'direct', '##ions', 'due', 'to', 'Lake', 'County', 'fire', '-', '#', 'CA', '##fire', '[SEP]']

```

Is it possible to make sure ‘@USER’ stays intact? Why is it getting split?

---

<div class="post-metadata">

**Author:** ![John6666](https://sea2.discourse-cdn.com/hellohellohello/user_avatar/discuss.huggingface.co/john6666/32/27664_2.png) [@John6666](/static-proxy?url=https%3A%2F%2Fdiscuss.huggingface.co%2Fu%2FJohn6666)%5C
**Post date:** [June 30, 2025, 3:04am UTC](/static-proxy?url=https%3A%2F%2Fdiscuss.huggingface.co%2Ft%2Ftokenizer-is-splitting-special-token%2F161147%2F2 "2025-06-30T03:04:15Z")

</div>

It seems more reliable to use `add_tokens()` rather than `additional_special_tokens`.

> <https://github.com/huggingface/transformers/issues/9747>
>
> I want add some special tokens such as \<POS\> \<CON\_START\> . But T5tokenizer/MT5t…okenizer both can't tokenize correctly after using additional\_special\_tokens parameter. It still split these special tokens to subwords.
> \<img width="1015" alt="截圖 2021-01-22 下午5 18 00" src="https://user-images.githubusercontent.com/26171212/105473581-3d88d100-5cd8-11eb-8568-6fedd19513e2.png"\>
> 
> 
> It works when using OpenAIGPTTokenizer additional\_special\_tokens parameter. It's clear that after declare additional\_special\_tokens parameter, OpenAIGPTTokenizer tokenize \<POS\> as one word rather split it.
> \<img width="979" alt="截圖 2021-01-22 下午5 54 57" src="https://user-images.githubusercontent.com/26171212/105475970-049e2b80-5cdb-11eb-8470-576fd8f38999.png"\>
> \<img width="697" alt="截圖 2021-01-22 下午5 55 10" src="https://user-images.githubusercontent.com/26171212/105475992-0962df80-5cdb-11eb-9cae-205b57818e95.png"\>
> 
> 
> 
> 
> The version of transformers is 4.2.2
> And I'm not sure this problem is related with \[issue624\](https://github.com/google-research/text-to-text-transfer-transformer/issues/624) in T5 which talk about SentencePiece extra vocab.
> 
> Thank you for your feedback

> <https://github.com/huggingface/tokenizers/issues/635>
>
> Hi, 
> I trained my own tokenizer using tokenizers library and want to use vocab …file with Fast Tokenizers.
> However, fast tokenizer split special tokens like below even \`PreTrainedTokenizerFast\` have special\_tokens.
> 
> \`\`\`python
> \>\>\> tokenizer = ElectraTokenizerFast('my\_vocab.txt', do\_lower\_case=False, model\_max\_length=512)
> \>\>\> tokenizer
> PreTrainedTokenizerFast(name\_or\_path='', 
> vocab\_size=32000, 
> model\_max\_len=512, 
> is\_fast=True, 
> padding\_side='right',
> special\_tokens={'unk\_token': '\[UNK\]', 'sep\_token': '\[SEP\]', 'pad\_token': '\[PAD\]', 'cls\_token': '\[CLS\]', 'mask\_token': '\[MASK\]'})
> \>\>\> tokenizer.tokenize("\[MASK\] is capital of France.", add\_special\_tokens=True)
> \['\[CLS\]', '\[', 'MA', '##SK', '\]', 'is', 'cap', '##ital', 'of', 'France', '.', '\[SEP\]'\]
> \`\`\`
> 
> but when I \`add\_special\_tokens\` manually, it works well
> 
> \`\`\`python
> \>\>\> tokenizer = ElectraTokenizerFast('my\_vocab.txt', do\_lower\_case=False, model\_max\_length=512)
> \>\>\> special\_tokens = {
> "unk\_token": "\[UNK\]",
> "sep\_token": "\[SEP\]",
> "pad\_token": "\[PAD\]",
> "cls\_token": "\[CLS\]",
> "mask\_token": "\[MASK\]" }
> \>\>\> tokenizer.add\_special\_tokens(special\_tokens)
> \>\>\> tokenizer.tokenize("\[MASK\] is capital of France.", add\_special\_tokens=True)
> \['\[CLS\]', '\[MASK\]', 'is', 'cap', '##ital', 'of', 'France', '.', '\[SEP\]'\]
> \`\`\`
> 
> 
> Should I add special tokens again with \`add\_special\_tokens\` manually, even if \`PreTrainedTokenizerFast\` have default special tokens?

---

<div class="post-metadata">

**Author:** ![torch-nn](https://avatars.discourse-cdn.com/v4/letter/t/e9bcb4/32.png) [@torch-nn](/static-proxy?url=https%3A%2F%2Fdiscuss.huggingface.co%2Fu%2Ftorch-nn)%5C
**Post date:** [June 30, 2025, 3:15am UTC](/static-proxy?url=https%3A%2F%2Fdiscuss.huggingface.co%2Ft%2Ftokenizer-is-splitting-special-token%2F161147%2F3 "2025-06-30T03:15:56Z")

</div>

I just tried `add_tokens` (like below) and its still not working:

```auto
wrapped_tokenizer = PreTrainedTokenizerFast(
    tokenizer_object=tokenizer,     
    unk_token="[UNK]",
    cls_token="[CLS]",
    sep_token="[SEP]",
    pad_token="[PAD]",
    mask_token="[MASK]",
)
wrapped_tokenizer.add_tokens(['HTTPURL', '@USER'])

```

---

<div class="post-metadata">

**Author:** ![John6666](https://sea2.discourse-cdn.com/hellohellohello/user_avatar/discuss.huggingface.co/john6666/32/27664_2.png) [@John6666](/static-proxy?url=https%3A%2F%2Fdiscuss.huggingface.co%2Fu%2FJohn6666)%5C
**Post date:** [June 30, 2025, 5:29am UTC](/static-proxy?url=https%3A%2F%2Fdiscuss.huggingface.co%2Ft%2Ftokenizer-is-splitting-special-token%2F161147%2F4 "2025-06-30T05:29:37Z")

</div>

Hmm… How about `save_pretrained` with `from_pretrained` way?

> [@Cannot create an identical PretrainedTokenizerFast object from a Tokenizer created by tokenizers library](/static-proxy?url=https%3A%2F%2Fdiscuss.huggingface.co%2Ft%2Fcannot-create-an-identical-pretrainedtokenizerfast-object-from-a-tokenizer-created-by-tokenizers-library%2F9317)%3A
>
> I can’t seem to create a “PreTrainedTokenizerFast” object from my original tokenizers tokenizer object that has the same proporties. This is the code for a byte pair tokenizer I have experimented on. The resulting fast tokenizer does not have a [PAD] token, and does not have any special tokens at all. tokenizer = ByteLevelBPETokenizer() tokenizer.preprocessor = pre\_tokenizers.BertPreTokenizer() tokenizer.normalizer = normalizers.BertNormalizer() tokenizer.train\_from\_iterator(doc…

> <https://github.com/huggingface/tokenizers/issues/777>
>
> I can't seem to create a "PreTrainedTokenizerFast" object from my original \`toke…nizers\` tokenizer object that has the same proporties. This is the code for a byte pair tokenizer I have experimented on. The resulting fast tokenizer does not have a \[PAD\] token, and does not have any special tokens at all. 
> \`\`\` 
> tokenizer = ByteLevelBPETokenizer()
> tokenizer.preprocessor = pre\_tokenizers.BertPreTokenizer()
> tokenizer.normalizer = normalizers.BertNormalizer()
> tokenizer.train\_from\_iterator(docs, vocab\_size=16\_000, min\_frequency=15, special\_tokens = \["\[UNK\]", "\[CLS\]", "\[SEP\]", "\[PAD\]", "\[MASK\]"\])
> tokenizer.\_tokenizer.post\_processor = processors.BertProcessing(
> ("\[SEP\]", tokenizer.token\_to\_id("\[SEP\]")),
> ("\[CLS\]", tokenizer.token\_to\_id("\[CLS\]")),
> )
> tokenizer.enable\_truncation(max\_length=256)
> tokenizer.enable\_padding(pad\_id=3, pad\_token="\[PAD\]")
> fast\_tokenizer = PreTrainedTokenizerFast(tokenizer\_object=tokenizer)
> \`\`\`
> 
> The result of printing the fast\_tokenizer is:
> \`\`\`
> PreTrainedTokenizerFast(name\_or\_path='', vocab\_size=16000, model\_max\_len=1000000000000000019884624838656, is\_fast=True, padding\_side='right', special\_tokens={})
> \`\`\`
> Which \`model\_max\_len\` and \`special\_tokens\` are wrong in it. Also, there is no pad\_token and pad\_token\_id in the fast\_tokenizer object. (the warning for pad\_token for example: \`Using pad\_token, but it is not set yet.\`) Have I done anything wrong, or is this not supposed to happen?
> 
> The versions of libraries I'm using:
> \`\`\`
> \['tokenizers 0.10.3',
> 'transformers 4.10.0.dev0'\]
> \`\`\`
