from fastai.vision.all import *
from fasterai.core.all import *
from fasterai.distill.all import *KnowledgeDistillation Callback
Distill a teacher into a student
KnowledgeDistillationCallback blends a distillation term into the student’s loss: at every batch it runs the teacher on the same inputs, compares the two models with the loss function you pass, and interpolates between that term and the ground-truth loss with weight (so at weight=0.9 the ground-truth term carries 0.1). This page trains one teacher (ResNet-34) and three students (ResNet-18), all at 64 px on the PETS cat/dog task.
1. Data
path = untar_data(URLs.PETS)
files = get_image_files(path/"images")
def label_func(f): return f[0].isupper()
dls = ImageDataLoaders.from_name_func(path, files, label_func, item_tfms=Resize(64))2. The teacher
A pretrained ResNet-34, fine-tuned for ten epochs:
teacher = vision_learner(dls, resnet34, metrics=accuracy)
teacher.unfreeze()
teacher.fit_one_cycle(10, 1e-3)| epoch | train_loss | valid_loss | accuracy | time |
|---|---|---|---|---|
| 0 | 0.746749 | 0.465374 | 0.878890 | 00:04 |
| 1 | 0.456684 | 0.337743 | 0.851150 | 00:03 |
| 2 | 0.306100 | 0.256743 | 0.896482 | 00:03 |
| 3 | 0.230790 | 0.197438 | 0.924222 | 00:03 |
| 4 | 0.175541 | 0.999262 | 0.843031 | 00:03 |
| 5 | 0.162185 | 7.491956 | 0.633965 | 00:03 |
| 6 | 0.114214 | 0.193958 | 0.928281 | 00:04 |
| 7 | 0.075693 | 0.171177 | 0.936401 | 00:04 |
| 8 | 0.038465 | 0.169076 | 0.941137 | 00:04 |
| 9 | 0.022227 | 0.171598 | 0.943843 | 00:04 |
from math import sqrt
def report(learn, name):
"Validation accuracy with its Wilson 95% interval"
n = len(learn.dls.valid_ds)
with learn.no_bar(): acc = float(learn.validate()[1])
k, z = round(acc*n), 1.96
p, d = k/n, 1 + z**2/n
c = p + z**2/(2*n)
h = z*sqrt(p*(1-p)/n + z**2/(4*n**2))
print(f"{name}: {acc:.2%} ({k}/{n}), Wilson 95% [{(c-h)/d:.2%}, {(c+h)/d:.2%}]")
report(teacher, "teacher, ResNet-34")teacher, ResNet-34: 94.38% (1395/1478), Wilson 95% [93.09%, 95.45%]
3. Student, no distillation
Same data, epochs and learning rate as the teacher; the students carry no pretrained weights:
student = Learner(dls, resnet18(num_classes=2), metrics=accuracy)
student.fit_one_cycle(10, 1e-3)| epoch | train_loss | valid_loss | accuracy | time |
|---|---|---|---|---|
| 0 | 0.619088 | 0.605188 | 0.689445 | 00:03 |
| 1 | 0.586678 | 0.566887 | 0.706360 | 00:03 |
| 2 | 0.546209 | 0.610709 | 0.667794 | 00:03 |
| 3 | 0.513684 | 0.498844 | 0.747632 | 00:03 |
| 4 | 0.463067 | 0.586233 | 0.752368 | 00:03 |
| 5 | 0.403649 | 0.653835 | 0.764547 | 00:03 |
| 6 | 0.356432 | 0.457676 | 0.814614 | 00:02 |
| 7 | 0.277365 | 0.373427 | 0.837618 | 00:02 |
| 8 | 0.213076 | 0.394300 | 0.828146 | 00:02 |
| 9 | 0.160289 | 0.389774 | 0.838972 | 00:02 |
report(student, "student alone")student alone: 83.90% (1240/1478), Wilson 95% [81.94%, 85.68%]
4. SoftTarget
SoftTarget compares the softened predictions of the two models. schedule=cos makes the weight of that term follow a cosine progression over training:
student = Learner(dls, resnet18(num_classes=2), metrics=accuracy)
kd = KnowledgeDistillationCallback(teacher.model, SoftTarget, schedule=cos)
student.fit_one_cycle(10, 1e-3, cbs=kd)| epoch | train_loss | valid_loss | accuracy | time |
|---|---|---|---|---|
| 0 | 0.627812 | 0.699146 | 0.635995 | 00:02 |
| 1 | 0.697857 | 0.745051 | 0.706360 | 00:03 |
| 2 | 0.847422 | 0.836012 | 0.741543 | 00:02 |
| 3 | 0.993758 | 0.988241 | 0.744926 | 00:02 |
| 4 | 1.140755 | 1.061720 | 0.788227 | 00:02 |
| 5 | 1.200129 | 1.146610 | 0.804465 | 00:02 |
| 6 | 1.133657 | 0.959932 | 0.847091 | 00:03 |
| 7 | 0.952126 | 0.933980 | 0.855210 | 00:02 |
| 8 | 0.804851 | 0.941943 | 0.859269 | 00:02 |
| 9 | 0.694702 | 0.928969 | 0.866712 | 00:03 |
report(student, "student + SoftTarget")student + SoftTarget: 86.67% (1281/1478), Wilson 95% [84.84%, 88.31%]
5. Attention, with the layers matched automatically
Intermediate-feature losses need pairs of layers, one in each model. match_feature_layers builds them: it runs one forward pass through each model, groups layers by output (H, W), and picks a match at each resolution.
student_model = resnet18(num_classes=2)
pairs = match_feature_layers(student_model, teacher.model, torch.randn(1, 3, 64, 64))
print(pairs)
student = Learner(dls, student_model, metrics=accuracy)
kd = KnowledgeDistillationCallback(
teacher.model, Attention,
pairs['student'], pairs['teacher'],
weight=0.9
)
student.fit_one_cycle(10, 1e-3, cbs=kd){'student': ['conv1', 'layer1', 'layer2', 'layer3', 'layer4'], 'teacher': ['0.0', '0.4', '0.5', '0.6', '0']}
| epoch | train_loss | valid_loss | accuracy | time |
|---|---|---|---|---|
| 0 | 0.087690 | 0.083192 | 0.700271 | 00:03 |
| 1 | 0.079434 | 0.069559 | 0.728011 | 00:02 |
| 2 | 0.069988 | 0.071774 | 0.747632 | 00:03 |
| 3 | 0.061195 | 0.056563 | 0.803112 | 00:04 |
| 4 | 0.054241 | 0.053276 | 0.811231 | 00:04 |
| 5 | 0.045464 | 0.052328 | 0.815968 | 00:04 |
| 6 | 0.038911 | 0.043625 | 0.870095 | 00:04 |
| 7 | 0.030366 | 0.041490 | 0.872124 | 00:04 |
| 8 | 0.022930 | 0.041212 | 0.884980 | 00:05 |
| 9 | 0.019471 | 0.040854 | 0.884980 | 00:06 |
report(student, "student + Attention")student + Attention: 88.50% (1308/1478), Wilson 95% [86.77%, 90.03%]
The teacher is a fastai vision_learner model, so its layers are named positionally: '0.0' is the body’s first convolution, '0.4', '0.5', '0.6' are layer1, layer2, layer3. The last pair maps the student’s layer4 onto '0', the body itself — the body’s output is the output of its last block, so this hooks the same tensor as '0.7' would.
Scope. PETS cat/dog labels, images resized to 64 px, ten epochs per arm at lr=1e-3; single run, no seed fixed. Every accuracy above is measured on the same 1478 validation images and printed with its Wilson 95% interval. The teacher is an ImageNet-pretrained ResNet-34 with a fastai head, the three students are resnet18(num_classes=2) trained from scratch — pretraining, not architecture, is the dominant difference between the first number and the other three. Each distillation arm also changes more than one thing with respect to the student trained alone: SoftTarget sets schedule=cos, Attention sets weight=0.9. So the four numbers show that the callback runs and what it produced here, not the size of an effect. This page measures no latency.
Summary
| Tool / Function | What it gives you |
|---|---|
KnowledgeDistillationCallback(teacher, loss, activations_student, activations_teacher, weight, schedule) |
A distillation term added to the student’s loss during fit |
match_feature_layers(student, teacher, x) |
Layer pairs matched by output resolution, for the feature-based losses |
SoftTarget |
Compares the two models’ softened predictions |
Attention |
Compares attention maps taken at the matched layers |
The losses exported by fasterai.distill.losses are SoftTarget, Logits, Mutual, DecoupledKD, Attention, ActivationBoundaries, FitNet and Similarity.
See Also
- Distillation Losses - Every distillation loss, and what each one compares
- KnowledgeDistillationCallback - The callback’s API reference
- Pruner - Structured pruning, to make the student smaller still
- Sparsifier - Unstructured sparsification