Fine-Tuning a Text Classifier
Fine-tuning updates some or all parameters of a pretrained model for a downstream objective. The learning rate, pooling rule, frozen layers, class weights, validation policy, and tokenizer are part of the experiment. This lesson demonstrates the mechanics locally; it does not pretend that a randomly initialized toy encoder is pretrained.
Prerequisites
Complete Pretraining and Transfer Learning and understand binary logits and gradient updates.
Learning goals
- separate encoder parameters from a task-specific classification head.
- mask padding during pooling and train a head before optionally unfreezing an encoder.
- keep pretrained Hugging Face loading outside deterministic CI.
Worked example
The fixed embedding table acts as a known local encoder. We freeze it and train only the linear head on six examples. This is a mechanical analogue of head-first fine-tuning, not a quality benchmark.
import torch
from torch import nn
torch.manual_seed(45)
input_ids = torch.tensor([[2, 4, 0], [2, 5, 0], [4, 2, 0], [3, 5, 0], [3, 4, 0], [5, 3, 0]])
labels = torch.tensor([1, 1, 1, 0, 0, 0], dtype=torch.float32)
encoder = nn.Embedding(6, 2, padding_idx=0)
with torch.no_grad():
encoder.weight.copy_(torch.tensor([
[0.0, 0.0], [0.0, 0.0], [1.0, 0.2], [-1.0, 0.2], [0.0, 0.1], [0.0, -0.1]
]))
encoder.weight.requires_grad_(False)
head = nn.Linear(2, 1)
optimizer = torch.optim.SGD(head.parameters(), lr=0.5)
mask = input_ids.ne(0)
for _ in range(60):
pooled = (encoder(input_ids) * mask.unsqueeze(-1)).sum(1) / mask.sum(1, keepdim=True)
logits = head(pooled).squeeze(1)
loss = nn.functional.binary_cross_entropy_with_logits(logits, labels)
optimizer.zero_grad(set_to_none=True)
loss.backward()
optimizer.step()
with torch.inference_mode():
probabilities = torch.sigmoid(head(pooled).squeeze(1))
predictions = probabilities.ge(0.5).to(torch.int64)
print(f"ids={tuple(input_ids.shape)} pooled={tuple(pooled.shape)} logits={tuple(logits.shape)}")
print(f"encoder_trainable={encoder.weight.requires_grad} head_trainable={head.weight.requires_grad}")
print(f"predictions={predictions.tolist()} accuracy={predictions.eq(labels.to(torch.int64)).float().mean():.1f}")
Output:
ids=(6, 3) pooled=(6, 2) logits=(6,)
encoder_trainable=False head_trainable=True
predictions=[1, 1, 1, 0, 0, 0] accuracy=1.0
Real fine-tuning must select hyperparameters and checkpoints on training/validation data. Unfreezing can improve adaptation but increases memory, compute, and overfitting risk. Class imbalance may require weighting, sampling, threshold selection, or a different objective; each changes what a probability or default threshold means.
This optional Hugging Face sketch is not executed. It may download artifacts and requires a separately pinned transformers==5.20.0 environment plus an explicit dataset implementation.
from transformers import AutoModelForSequenceClassification, AutoTokenizer, Trainer, TrainingArguments
model_id = "distilbert/distilbert-base-uncased"
revision = "PIN_A_REVIEWED_COMMIT_SHA"
tokenizer = AutoTokenizer.from_pretrained(model_id, revision=revision)
model = AutoModelForSequenceClassification.from_pretrained(model_id, revision=revision, num_labels=2)
arguments = TrainingArguments(output_dir="artifacts/text-classifier", eval_strategy="epoch")
trainer = Trainer(model=model, args=arguments, train_dataset=train_dataset, eval_dataset=validation_dataset)
That block is a manual/download boundary and intentionally has no fabricated output.
Common mistakes
- Tokenizing before splitting or fitting label/preprocessing state on all data.
- Selecting epochs, learning rate, threshold, or checkpoint using test results.
- Pooling padding positions or assuming every architecture uses the same pooled output.
- Using accuracy alone with class imbalance or asymmetric harms.
- Fine-tuning every layer by default without a resource and overfitting comparison.
Practice
Unfreeze the local encoder only after the head has trained. Use a smaller encoder learning rate than head learning rate, and explain which validation evidence would justify the added complexity.
Next steps
Generation, Decoding, and Randomness moves from classification logits to repeated token selection.
Primary sources
Checked 2026-10-16: Hugging Face text-classification guide, Hugging Face Trainer, Hugging Face tokenizer API, PyTorch Embedding, and PyTorch binary cross-entropy with logits.