You've probably noticed that most production ML systems don't work with raw text—they need structure, vectors, and context. NLP basics for developers: practical tips and trade-offs covers the essential pipeline that turns messy user input into something a model can actually learn from. We'll walk through tokenization, embeddings, transformers, and pretrained models with real trade-offs you'll face in the field.
Tokenization splits text into units
Tokenization is your first line of defense. It takes a string like "We're hiring engineers in SF." and breaks it into discrete tokens that a model can process. The question isn't whether to tokenize—it's how.
At the simplest level, you might split on whitespace. That works for English sentences with clear word boundaries, but breaks down instantly when you hit contractions, punctuation, or multilingual text. "We're" becomes ["We", "'re"], which loses semantic information. Punctuation attached to words ("SF.") creates mismatches during inference if your training data stripped it differently.
Most teams use subword tokenization—typically BPE (Byte Pair Encoding) or WordPiece. These algorithms learn which character sequences appear frequently together and merge them. So "tokenization" might become ["token", "##ization"] instead of splitting into characters. The ## prefix signals a continuation of the previous token.
Here's why this matters in production: a vocabulary of 50,000 tokens (common for transformers) handles rare words better than a 10,000-token vocabulary, but increases model size and slows inference. We typically use a tokenizer provided by the pretrained model you're loading—like the ones in Hugging Face Transformers. Don't build your own unless you have very specific domain needs (like trading symbols or medical codes).
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")
tokens = tokenizer.encode("We're hiring engineers in SF.",
add_special_tokens=True)
print(tokens)
# [101, 1045, 1005, 2572, 4688, 3319, 1999, 3358, 119, 102]
decoded = tokenizer.decode(tokens)
print(decoded)
# "[CLS] we ' re hiring engineers in sf . [EOS]"Notice the special tokens [CLS] and [EOS]. The model needs these markers. [CLS] aggregates sentence-level information for classification tasks. Forgetting to include them causes a silent accuracy drop that's maddening to debug.
Embeddings turn tokens into vectors
Tokens are just integers. A model can't learn anything from the number 42 in isolation—it needs to know that token 42 represents something semantically related to token 150. Embeddings solve this: they're learned dense vectors, typically 768 or 1024 dimensions for transformers, where similar tokens end up in nearby regions of vector space.
An embedding lookup table maps each token ID to its vector. During training, these vectors update based on the language task (predicting masked tokens, next-sentence prediction, etc.). After training, you freeze them or fine-tune them lightly for your downstream task.
The trade-off: larger embedding dimensions capture finer semantic distinctions but increase memory and compute. A 30,000-token vocabulary with 768-dimensional embeddings needs 23 million parameters just for that layer. Most of the time, using a pretrained embedding from a model like BERT or GPT is faster and better than training your own from scratch unless you're in a domain where the pretrained vocabulary fundamentally misses your terms.
One practical decision: when should you fine-tune embeddings versus freezing them? Fine-tuning helps if your domain is specialized (e-commerce product descriptions, medical records, trading chat). Freezing them saves compute and reduces overfitting when you have little labeled data. A common middle ground in production systems is to freeze embeddings for the first few epochs, then unfreeze them with a low learning rate.
import torch
from transformers import AutoModel
model = AutoModel.from_pretrained("bert-base-uncased")
embedding_layer = model.embeddings.word_embeddings
# Embedding table shape: (vocab_size, hidden_size)
print(embedding_layer.weight.shape)
# torch.Size([30522, 768])
# Freeze embeddings
for param in embedding_layer.parameters():
param.requires_grad = FalseTransformers model context
Here's where things get interesting. Before transformers, recurrent networks (LSTMs, GRUs) processed tokens sequentially, which was slow and lost information over long sequences. Transformers use self-attention to let each token "look at" every other token in parallel, capturing long-range dependencies efficiently.
Self-attention computes three matrices—query, key, value—for each token. Intuitively, a token's query asks "what matters to me?" and is matched against all keys to score relevance. High-scoring tokens contribute more to the final representation via weighted values. Multiple attention heads (typically 12) attend to different aspects: one head might focus on grammatical structure, another on semantic relationships.
Why does this matter for deployment? Transformers are quadratic in sequence length during inference. A 512-token sequence is fine. A 4,096-token sequence becomes expensive, and 16,000-token sequences (some models support this now) are only practical in batch or offline settings. If you're building a real-time chat or autocomplete system, you'll hit this limit quickly.
In production, teams use a few strategies: chunk long documents, use sliding windows with overlap, or truncate to a fixed length (accepting information loss). The choice depends on your use case. For customer support ticket classification, truncating to 512 tokens loses maybe 2% of tickets entirely; for a search engine, it's unacceptable. Know your trade-off.
Another production concern: attention can be brittle. An out-of-vocabulary token or a typo can propagate through attention heads and confuse the model more than you'd expect. Robust systems add preprocessing steps: spelling correction, normalization, or fallback patterns.
Pretrained models solve most tasks
This is the truth most people don't internalize fast enough: training a transformer from scratch is rarely worth it. Pretrained models—BERT, RoBERTa, GPT-2, T5, Mistral—are trained on massive corpora with self-supervised objectives. They've already learned syntax, semantics, and world knowledge. Your job is fine-tuning.
Fine-tuning means taking a pretrained checkpoint and updating its weights on your labeled dataset for your specific task. For classification (is this email spam?), you typically add a linear head on top. For sequence-to-sequence tasks (summarization, translation), you might use an encoder-decoder architecture. For open-ended generation, you use a language model directly.
The practical question: which pretrained model? BERT-family models (BERT, RoBERTa, DistilBERT) are great for classification and NLU tasks. They're fast, well-documented, and come in many sizes. GPT-family models are better for generation. T5 excels at multitask learning via prompts. Mistral or Llama might be overkill for classification but excellent for generation when you need reasoning.
A real scenario: we recently built a product tagging system for an e-commerce platform. We compared DistilBERT (6 layers, 12M parameters) against BERT (12 layers, 110M parameters). DistilBERT was 40% faster and almost as accurate. We fine-tuned for 2 epochs on 50,000 labeled products, achieving 94% accuracy on a test set with 200 categories. Training took 20 minutes on a single GPU. Training a model from scratch would have taken weeks and likely underperformed.
from transformers import AutoTokenizer, AutoModelForSequenceClassification, Trainer, TrainingArguments
import torch
# Load pretrained model
model = AutoModelForSequenceClassification.from_pretrained(
"distilbert-base-uncased",
num_labels=200 # Your task-specific label count
)
tokenizer = AutoTokenizer.from_pretrained("distilbert-base-uncased")
# Prepare dataset
def tokenize_function(examples):
return tokenizer(examples["text"], padding="max_length", truncation=True)
# Fine-tune
training_args = TrainingArguments(
output_dir="./product_tagger",
num_train_epochs=2,
per_device_train_batch_size=32,
per_device_eval_batch_size=64,
learning_rate=2e-5,
warmup_steps=500,
weight_decay=0.01,
)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=train_dataset,
eval_dataset=eval_dataset,
)
trainer.train()One crucial consideration: cost. API-based models (OpenAI, Anthropic) outsource the compute. Running your own model (via Hugging Face Transformers or vLLM) requires GPU infrastructure. For a small SaaS startup, API calls might be cheaper initially. As volume grows, self-hosted becomes economic. We've found the crossover happens around 100K requests per month for text classification.
Common mistakes
- Forgetting special tokens during inference: Your training code added
[CLS]and[PAD]tokens, but your inference script tokenizes differently. Result: silent accuracy drop. Always use the same tokenizer for training and serving. - Not batching correctly for GPU: Sending single examples to a model wastes GPU memory. Batch at least 32 examples. Use padding and attention masks so the model ignores padded tokens.
- Truncating too aggressively: Cutting every document to 128 tokens "for speed" might discard critical information. Measure the impact first. Most classification tasks work fine with 256–512 tokens.
- Mismatching preprocessing between train and inference: Lowercase your text during training? Do it during inference. Strip special characters in training data? Do it in production. Inconsistency tanks accuracy.
- Using a model too large for your latency budget: An 8B parameter model gives amazing quality but 500ms latency. Your API timeout is 100ms. You need DistilBERT or a smaller quantized variant, even if accuracy drops 2–3%.
- Ignoring embedding layer fine-tuning strategies: Freezing embeddings when they're misaligned with your domain wastes model capacity. Not freezing them when data is scarce causes overfitting. Experiment and measure.
Key takeaways
- Tokenization is task-specific. Use the tokenizer from your pretrained model; don't roll your own unless you have exotic requirements.
- Embeddings are learned vectors where similar tokens cluster together. Decide early whether to freeze or fine-tune based on domain specificity and labeled data volume.
- Transformers use self-attention to capture context but are quadratic in sequence length. Production systems need strategies for long documents: chunking, truncation, or sliding windows.
- Pretrained models are your foundation. Fine-tune them on your labeled data rather than training from scratch. The speed and quality gains are substantial.
- Consistency between training and inference preprocessing is non-negotiable. Small mismatches compound silently.
- Choose model size based on your latency and accuracy trade-offs, not on benchmark leaderboards. A smaller model deployed in time is better than a larger model that never ships.






Comments
Loading comments…