Use when choosing how to tokenize text or which transformer type fits an NLP task, when a tokenizer over-fragments non-English text or inflates token cost, when picking a language metric, or when classification, NER or summarization output looks wrong and it is unclear whether the tokenizer, the architecture or the metric is at fault. Covers subword tokenizers, encoder versus decoder versus encoder-decoder choice, sentence embeddings, and the metric families. NOT retrieval or vector search (that is `embeddings-search`), NOT the RAG loop (that is `rag`), NOT prompt wording (that is `prompt-engineering`), NOT training the network (that is `finetuning`).
npx skills add https://github.com/ericrisco/rsc-harness --skill nlp
You own the language-modeling discipline: how raw text becomes tokens, which transformer
architecture fits a task, and which metric actually tells you whether it worked. When the
question is "which tokenizer," "BERT or GPT or T5 for this," "why does my Catalan text cost 3×
the tokens," or "is this BLEU score meaningful," this is the skill. You stop at retrieval, the
RAG loop, prompt wording, and the training step itself — those route out (below).
→ ../embeddings-search/SKILL.md. Sentence embeddings *live
here* as a task; using them to *retrieve* is theirs.
../rag/SKILL.md.../prompt-engineering/SKILL.md.../finetuning/SKILL.md+ ../deep-learning/SKILL.md.
../training-data/SKILL.md.Pick the architecture from the task's *shape*, not from what is trendy. A decoder LLM can
technically classify, but a fine-tuned encoder is smaller, faster, cheaper, and usually more
accurate on a fixed-label task.
| Task shape | Architecture | Why | Example families* |
|---|---|---|---|
| Understand / label a whole input (classification, NER, extractive QA, similarity) | Encoder (bidirectional) | Attends to the full sentence both directions; cheap to fine-tune and to serve | BERT, RoBERTa, DistilBERT, ModernBERT |
| Free-form generation, chat, few-shot | Decoder (autoregressive) | Attends only to prior tokens; predicts the next token | GPT-style, Llama, Gemma, Qwen |
| Transform input → new text (summarize, translate, generative QA) | Encoder-decoder / seq2seq | Encoder reads all of the source, decoder writes conditioned on it | T5 / FLAN-T5, BART, mT5 |
\* Architecture families are stable; specific checkpoints and their licenses are not — check the
HF model card before you commit (licenses change; see open-weights). ModernBERT (2024) is a
current long-context encoder; verify the latest at author time.
The two most common own-goals: reaching for a 7B decoder to do sentiment on 5 classes (an
encoder does it for a fraction of the cost), and forcing an encoder to *generate* (it cannot —
it has no decoder).
Every downstream number depends on this step, and its failures are silent. The single
load-bearing rule:
> **Load the tokenizer that shipped with the checkpoint, and use the same one at train and
> inference.** AutoTokenizer.from_pretrained(same_checkpoint). A train/inference tokenizer
> mismatch — different vocab, different special tokens, different casing/normalization — maps
> text to token ids the model never saw and corrupts everything downstream with no error.
from transformers import AutoTokenizer # transformers current major ~v5 (verify at author time)
tok = AutoTokenizer.from_pretrained("bert-base-cased")
enc = tok("Tokenizers matter.", return_offsets_mapping=True)
tok.convert_ids_to_tokens(enc["input_ids"])
# ['[CLS]', 'Token', '##izers', 'matter', '.', '[SEP]'] — note WordPiece '##' continuation + added specials
The four algorithms (full mechanics in references/tokenization.md):
| Algorithm | Builds vocab by… | Applies by… | Used by |
|---|---|---|---|
| BPE | merging the most frequent adjacent pair, repeatedly | split to chars, replay learned merges | GPT-2 (byte-level), many |
| WordPiece | merging pairs that maximize a likelihood score | longest-match subword from the front (## continuations) | BERT family |
| Unigram (SentencePiece) | start large, *remove* tokens that least hurt corpus likelihood | most-probable segmentation | T5, ALBERT, mT5 |
| Byte-level BPE | BPE over the 256 raw bytes, not Unicode chars | same as BPE on bytes | GPT-2, RoBERTa |
[UNK]. Base vocab is exactly 256 (all byte values), so everyemoji, accent, and script maps to *some* byte sequence — nothing falls out as unknown
(verified: HF NLP course ch.6). WordPiece/word-level tokenizers *do* have [UNK] and lose OOV
content.
▁ meta-symbol), sodecode(encode(x)) == x without language-specific detokenization rules. That is why it
dominates multilingual models.
Special tokens are not decoration. [CLS]/<s> carries the pooled sentence
representation for classification; [SEP]/</s> marks segment/end; [PAD] fills a batch (and
must be masked out via attention_mask); [MASK] is the MLM target; [UNK] is the fallback.
Names differ by model ([CLS] in BERT vs <s> in RoBERTa) — another reason to never hand-roll
the tokenizer.
Why it matters — three concrete costs:
sentence = cheaper calls and more room in the window.
[UNK] throws away content it can'trepresent; byte-level/SentencePiece degrade gracefully instead.
meaning costs more tokens, more money, and more latency (section 5).
from transformers import pipeline
clf = pipeline("text-classification", model="distilbert-base-uncased-finetuned-sst-2-english")
clf("The service was slow but the food was incredible.")
# [{'label': 'POSITIVE', 'score': 0.99...}]
Metric: accuracy on balanced data; macro-F1 the moment classes are imbalanced (accuracy
lies when 95% of rows are one class).
Labels are B-/I-/O spans aligned to subword tokens: the first subword of a word gets the
label, continuation subwords and special tokens get -100 (ignored by the loss). Evaluate with
seqeval at the entity level, never per-token accuracy (per-token accuracy is inflated by
the flood of O tokens).
from transformers import pipeline
ner = pipeline("token-classification", aggregation_strategy="simple")
ner("Ada Lovelace worked in London.")
# groups subwords back into entities: PER 'Ada Lovelace', LOC 'London'
summ = pipeline("summarization", model="facebook/bart-large-cnn")
summ(long_article, max_length=130, min_length=30)
Metric: ROUGE for summarization, BLEU/chrF for translation — with the heavy caveat in
section 4.
from sentence_transformers import SentenceTransformer # sentence-transformers ~v5 (verify)
model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")
emb = model.encode(["The weather is lovely today.", "It's so sunny outside!"])
model.similarity(emb, emb) # semantic textual similarity / clustering / paraphrase mining
Producing/judging embeddings for retrieval (model choice, chunking, recall@k, rerankers) is
embeddings-search, not here.
| Task | Primary metric | Catches | Trap |
|---|---|---|---|
| Classification | accuracy + macro-F1 | wrong labels | accuracy hides minority-class failure |
| NER / token | entity-level F1 (seqeval) | missed/partial spans | per-token accuracy is inflated by O |
| Translation | BLEU / chrF | n-gram overlap w/ reference | weak on meaning; chrF better for morphology |
| Summarization | ROUGE (1/2/L) | recall of reference n-grams | rewards copying; blind to faithfulness |
| Generation (LM) | perplexity | how well the model predicts held-out text | tokenizer-dependent — not comparable across tokenizers |
| Open-ended / chat | LLM-as-judge + human | quality overlap metrics miss | judge bias (position, verbosity, self-preference) |
The caveat that governs this whole section: BLEU, ROUGE, and chrF are n-gram/character
overlap metrics and **correlate weakly with human judgment on open-ended and creative
generation** — they reward matching the reference's exact phrasing, so a correct paraphrase
scores low and a fluent-but-wrong copy scores high (well documented; e.g. the summarization and
MT-evaluation literature). Use them for regression tracking on a fixed reference set, never
as the final verdict on quality. For open-ended output, use an **LLM-as-judge rubric plus a
human spot-check** — and know the judge has its own biases (position, verbosity, self-preference),
so pin the rubric and randomize order.
Perplexity = exp(mean token NLL): lower means the model predicts held-out text better. It is
tokenizer-dependent, so two models with different tokenizers have non-comparable perplexities
— only compare within the same tokenizer/vocab. Runnable snippets for seqeval, sacrebleu, ROUGE,
perplexity, and an LLM-judge harness are in references/evaluation.md.
The English-centric trap: a tokenizer whose vocab was learned mostly on English **over-fragments
other scripts**. The same sentence in Ukrainian, Arabic, Hindi, or even accented Catalan can take
2–15× more tokens than its English equivalent (Petrov et al., *Language Model Tokenizers
Introduce Unfairness Between Languages*, NeurIPS 2023). That "fertility" (tokens per word)
inflation is a triple tax:
long documents truncate sooner.
exactly the users the tool already serves worst.
Mitigations: prefer a multilingual tokenizer/model (mT5, XLM-R, a SentencePiece-based model)
whose vocab actually covers your languages; measure fertility on your own corpus (tokens per
word, per language) before you commit; and don't benchmark cost or latency only on English.
no error. The most expensive bug in this skill.
to task shape.
regressions with them; judge quality with an LLM-judge + human.
O tokens inflate accuracy toward 1.0.shift (Llama = Meta Community license, not OSI-open; Gemma = custom terms; etc.).
embeddings-search — retrieval embeddings, chunking, recall@k,reranking. NLP owns *making/judging* sentence embeddings as a task; using them to search is theirs.
rag — the full retrieve→generate answer loop and groundedness.prompt-engineering — the wording of the prompt.finetuning + deep-learning — actuallytraining/adapting the network (this skill picks the type and metric; those move the weights).
training-data — building the labeled corpus you train on.attention_mask handled (padding masked, -100 on ignored labels).only as a regression signal, LLM-judge + human for open-ended.
references/tokenization.md — BPE/WordPiece/Unigram/byte-level training mechanics, specialtokens per family, offset mapping, and a fertility-measuring snippet.
references/evaluation.md — runnable seqeval, sacrebleu (BLEU/chrF), ROUGE, perplexity, and anLLM-as-judge rubric, with when each lies.
Take ericrisco/nlp from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.