Approach summary: create small deterministic tokenization examples and assert label length and per-token label mapping. Tests cover subword splitting (word -> multiple tokens), truncation (labels truncated or shifted), and special tokens (CLS/SEP added). Use a real tokenizer (HuggingFace) or a minimal mock to reproduce off-by-one.Example unit tests (pytest-style):python
from transformers import AutoTokenizer
import pytest
tok = AutoTokenizer.from_pretrained("bert-base-uncased")
def align_labels(tokens, word_labels):
# Implementation under test: maps word-level labels to token-level labels
# e.g., propagate first subword label to following subwords or use -100 for them
...
def tokens_from_text(text):
return tok(text, return_offsets_mapping=True, add_special_tokens=True)
def test_subword_alignment():
text = "playing"
# "play" + "##ing" -> two tokens
enc = tok(text, return_offsets_mapping=True, add_special_tokens=True)
labels = ["VERB"] # one word-level label
token_labels = align_labels(enc, labels)
# Expect same label on first subtoken, and -100 (or same) on continuation depending policy
assert token_labels[1] == "VERB" or token_labels[1] == -100
def test_special_tokens_shift():
text = "John"
enc = tok(text, add_special_tokens=True)
labels = ["B-PER"]
token_labels = align_labels(enc, labels)
# ensure label aligns to token index of "John", not off by one due to [CLS]
cls_index = 0
assert token_labels[cls_index] in (None, -100)
def test_truncation_behavior():
text = "A " + "word " * 1000
enc = tok(text, truncation=True, max_length=10, return_overflowing_tokens=False)
labels = ["O"] * (len(text.split()))
token_labels = align_labels(enc, labels)
assert len(token_labels) == enc['input_ids'].__len__()
Minimal input-output pair that reveals an off-by-one (subword + special token):- Text: "Unhappy"- Tokenization (bert-base-uncased): ["[CLS]", "un", "##happy", "[SEP]"]- Word label: ["ADJ"]If implementation mistakenly shifts labels by one, you might see labels aligned as:["[CLS]": "ADJ", "un": -100, "##happy": -100, "[SEP]": -100]Correct alignment should map "ADJ" -> token index 1 ("un") (or index 2 depending whether you keep CLS). A failing assertion: token_labels[1] == "ADJ" will catch off-by-one.Why these tests catch bugs:- Subword test ensures multi-token words don't drop or shift labels.- Special-token test ensures you don't assign labels to [CLS]/[SEP] or shift everything by their presence.- Truncation test ensures labels/truncation are consistent and lengths match.Best practice: run with both policies (propagate labels to subwords vs mark as -100) and assert behavior explicitly. Mock tokenizer offsets for deterministic unit tests.