Paper · 2022-08-10 · cs.CV, cs.LG
Patching open-vocabulary models by interpolating weights
PAINT patches a CLIP model for one task. It fine-tunes the visual encoder with a frozen text-derived head, then linearly interpolates weights between the zero-shot and fine-tuned models with one mixing coefficient alpha. On nine tasks where zero-shot CLIP is poor, ViT-L/14 gains 15 to 60 points while ImageNet stays within one point (author-reported). Also covers multi-task and broad transfer.
Model mergingVision-language pre-trainingContinual learningTransfer learningmodel-mergingweight-interpolationfine-tuningcliprobustness
arXivPDFCodeMarkdown9 min read
AI-compiled by claude-haiku-5-5 on 2026-10-19, not yet reviewed by a human. Every claim cites its source; check it when a detail matters.
In plain words
CLIP-style models can label images with any list of class names you type, but they are weak on some specific jobs, such as reading digits or traffic signs. The usual fix is to fine-tune the model on that job. That helps the job, but the model often gets worse at everything else it could do before.
This paper's method, called PAINT, is a two-step recipe. First, fine-tune the model on the weak task. Second, mix the old and new weights with a single dial between 0 and 1. A low setting stays close to the original model, a high setting stays close to the fine-tuned one, and something in between can keep most of the new skill while losing little on the old tasks. No new parameters are added, so the model still accepts any class names.
The authors report large gains on nine weak tasks while ImageNet accuracy barely moves. They also find that patching on one task can help a related task with different class names, and that the method works better on larger models.
TL;DR
- Two steps: fine-tune the visual encoder on the patch task, then interpolate between the zero-shot and fine-tuned weights with alpha §2.
- No task-specific parameters: the classification head comes from the frozen CLIP text encoder §3 app. C.
- ViT-L/14 gains 15 to 60 points on nine weak tasks, with ImageNet down less than 1 point (author-reported) §4 §1.
- Broad transfer: patching on one task raised accuracy on a different task with disjoint classes in 7 of 8 pairs §6 tab. 2.
- The authors report no extra cost at fine-tuning or inference time versus standard fine-tuning §1.
Problem
Zero-shot open-vocabulary models are often far from optimal on specific classification tasks §1. Fine-tuning fixes the target task but can cause catastrophic forgetting on tasks where the zero-shot model was already adequate, and a fine-tuned model with a fixed head loses the free-form, text-defined class interface §1. Re-training an open-vocabulary model from scratch with the new data is the alternative, and the authors call it practically infeasible in most settings §1. The goal is to improve accuracy on a patch task without reducing accuracy on a supported task such as ImageNet §1 §3.
The paper assumes labelled data for the patch task, and a held-out validation set for the supported task and the patch task, used to choose the mixing coefficient §2.
Method
Patching on one task §2. Let the zero-shot weights be theta_zs and the patch task be D_patch. A representative supported task D_supp is used for checking, not for training §2.
Step 1 fine-tunes theta_zs on training data from D_patch to get theta_ft. Step 2 interpolates: theta_patch = (1 - alpha) * theta_zs + alpha * theta_ft, with alpha in [0, 1] §2, steps 1 and 2. Alpha is chosen on held-out validation sets of D_supp and D_patch §2. The interpolation applies to the visual encoder only; the text encoder and the head stay fixed §3 app. B app. C.
Frozen head. Instead of a learned classifier, the head is the output of CLIP's text tower for each class name, so no class-specific parameter is added §3 app. C. App. C reports that freezing it has a negligible effect on accuracy for MNIST, EuroSAT and SUN397, over three seeds app. C.
Patching on several tasks §2 §5. The paper compares three strategies:
- Joint: merge all patch datasets into one task, then patch once §5.
- Sequential: patch on each new task in turn, starting each step from the last patched model §5.
- Parallel: fine-tune each task separately from theta_zs, then mix. The authors search one scalar alpha against the average of the fine-tuned models, because searching one alpha per task was impractical §5.
Joint patching needs data for every task at once. Sequential patching fits a stream of tasks. Parallel patching can use distributed hardware §5.
Broad transfer §6. A dataset is split into two disjoint sets of classes, task A and task B. Patching on A is then tested on B §6. Patching on one dataset and testing on a different dataset with related content is also tested §6 tab. 2.
Scale §4.1. The paper measures how far a patched model is from the best interpolation, with accuracy distance to optimal (eq. 1), and finds the gap shrinks as models grow §4.1 fig. 2. Representations and weights of the zero-shot and fine-tuned models become more similar at larger scale §4.1 fig. 2 app. D.
# PAINT: patch one task by interpolating weights [§2]
# Input: zeroshot_weights (visual encoder), patch_data, alphas
# Output: one patched encoder per candidate alpha
def patch(zeroshot_weights, patch_data, alphas):
# Step 1: fine-tune on the patch task, head frozen [§2, step 1]
finetuned_weights = finetune(zeroshot_weights, patch_data)
patched_models = {}
for alpha in alphas:
# Step 2: interpolate every tensor [§2, step 2]
# [code: src/patch.py#L48-L51]
patched_weights = {
name: (1 - alpha) * zeroshot_weights[name]
+ alpha * finetuned_weights[name]
for name in zeroshot_weights
}
patched_models[alpha] = patched_weights
# Choose alpha on held-out validation of supported and patch tasks [§2]
return patched_models# Fine-tuning used in step 1 [§3] [app. C] [code: src/finetune.py#L18-L125]
# Input: encoder (CLIP visual tower), head (frozen, from text encoder), data
# Output: fine-tuned encoder
def finetune(encoder, head, data):
# Only encoder parameters are trained; the head is frozen [§3]
optimizer = adamw(encoder.parameters(), lr=1e-5, weight_decay=0.1)
# Linear warm-up, then cosine decay to zero
# [§3] [code: src/utils.py#L19-L31]
scheduler = cosine_with_warmup(optimizer, warmup_steps=200)
for step in range(2000): # "2000 iterations", batch size 128 [§3]
images, labels = next_batch(data, batch_size=128)
# Normalised features times frozen class weights
# [code: src/modeling.py#L51]
logits = head(encoder(images))
loss = cross_entropy(logits, labels)
loss.backward()
# Clip gradient norm at 1.0 (not in the paper)
# [code: src/finetune.py#L95]
clip_grad_norm(encoder.parameters(), max_norm=1.0)
optimizer.step()
scheduler.step()
optimizer.zero_grad()
return encoderHyperparameters
Fine-tuning and interpolation values. Where the paper and the code differ, the row says so; see Paper vs code.
| Name | Value | Where | Notes |
|---|---|---|---|
| Backbone | CLIP ViT-L/14 (main); ViT-B/32, ViT-B/16, ResNets | §3 app. G | Text tower kept as frozen head |
| Trainable parameters | visual encoder only | §3 app. C | No new parameters; head frozen |
| Batch size | 128 | §3 | Same in code default args.py |
| Training length | 2000 iterations | §3 | Code counts epochs; see Paper vs code |
| Learning rate | 1e-5 | §3 | Code default is 1e-3; README sets 1e-5 |
| Warm-up | 200 steps | §3 | Code default is 500 |
| LR schedule | cosine annealing | §3 | Linear warm-up, then cosine to zero utils.py |
| Optimizer | AdamW | §3 | finetune.py |
| Weight decay | 0.1 | §3 | args.py |
| Label smoothing | not stated | §3 | Code default 0.0 args.py |
| Gradient clipping | not stated | not in paper | Code clips at norm 1.0 finetune.py |
| Mixing coefficient alpha | in [0, 1] | §2 | Tables use a grid from 0 to 1 in steps of 0.05 app. O |
| Alpha selection | held-out validation, average of supported and patch accuracy | §2 §3 | Not implemented in the code; see Paper vs code |
| Combined accuracy | mean of supported and patch accuracies | §3 footnote | Used for the multi-task comparisons §5 |
| Supported task | ImageNet (also CIFAR-10, CIFAR-100, Food101, STL-10) | §3 | Split sizes in app. A |
| Patch tasks | Cars, DTD, EuroSAT, GTSRB, KITTI, MNIST, RESISC45, SUN397, SVHN | §3 | Split sizes in app. A |
| Multi-task scalar alpha | one alpha for parallel patching | §5 app. J | Mixes zero-shot with the mean of fine-tuned models |
| Compute | about 10 thousand GPU-hours in total | app. N | NVIDIA A40 GPUs |
Results
Author-reported. Setup: CLIP ViT-L/14 unless stated; combined accuracy is the mean of supported and patch-task accuracy §3.
| Setting | Metric | Baseline | This paper | Source |
|---|---|---|---|---|
| Nine patch tasks, ViT-L/14 | accuracy gain on each task | zero-shot | +15 to +60 points | §1 §4 |
| Nine patch tasks, ViT-L/14 | ImageNet accuracy change | zero-shot | less than -1 point | §1 §4 |
| Joint patching, nine tasks, ViT-L/14 | combined accuracy gain | unpatched | +15.8 points | §5 |
| Joint vs. nine specialised models, ViT-L/14 | combined accuracy gap | multiple models | -0.5 points | §5 |
| Joint patch, ViT-B/32 vs unpatched ViT-L/14 | combined accuracy | unpatched ViT-L/14 | +6.1 points; ViT-L/14 needs about 12x more GMACs | §5 |
| Sequential patch, ViT-B/32, no interpolation | combined accuracy change | unpatched | -4.6 points | §5 |
| Sequential patch, ViT-B/32, with interpolation | combined accuracy change | unpatched | +11 points | §5 |
| Patch on A, unseen classes in B (9 datasets) | accuracy gain on B | unpatched | +0.8 to +19.4 points | tab. 1 |
| Patch on A, related dataset B (8 pairs) | accuracy gain on B | unpatched | +1.1 to +19.2 points in 7 pairs; -1.3 for EuroSAT to RESISC45 | tab. 2 |
| Typographic attacks, real-world set | accuracy gain | unpatched | +41 points; ImageNet less than -1 point | §7 |
| CLEVR counting, unseen classes | accuracy | 59% | over 99%; ImageNet less than -0.5 points | §7 |
| VQA v1 (multiple choice), ViT-L/14 | accuracy gain | unpatched | +18 points; ImageNet less than -1 point | §7 app. M |
| Scale | accuracy distance to optimal | smaller models | shrinks as models grow | §4.1 fig. 2 |
Per-dataset numbers for each model and alpha are in app. O (single-task tables), with multi-task per-dataset numbers in app. J.
Paper vs code
Official repository: https://github.com/mlfoundations/patching, linked in the
paper's footnote §7. Code commit 3ca86220b926849ea5539531722ba7143f67a30e.
The code covers single-task patching only: src/patch.py takes one
--train-dataset patch.py. I found no joint, sequential or
parallel multi-task code in src/.
Differences:
- Warm-up. Paper: 200 steps §3. Code default: 500 args.py. The README sample command does not override it README.md.
- Length. Paper: 2000 iterations §3. Code:
--epochs, default 10 args.py. The README sample uses 5 epochs on MNIST README.md. The step count therefore depends on the dataset size and is not 2000. - Learning rate. Paper: 1e-5 §3. Code default: 1e-3 args.py. The README sample uses 1e-5, which matches the paper.
- Gradient clipping. Code clips the global norm at 1.0 finetune.py. The paper does not mention clipping.
- Alpha selection. Paper: alpha is picked on held-out validation sets §2.
patch.pyevaluates every alpha in its--alphalist and saves each model patch.py. It contains no selection step, so choosing alpha is left to the user. - Head. Paper: the head is the frozen output of CLIP's text tower §3 app. C. Code builds it from several prompt templates per dataset, averages them, normalises, and multiplies by the exponential of CLIP's logit scale heads.py. The paper does not say which prompts it used for the head.
- Checkpoints. Code saves whole modules with
pickleutils.py. Loading them requires the same code and classes.
No other discrepancies found in the parts of the code I read (src/modeling.py,
src/finetune.py, src/patch.py, src/utils.py, src/heads.py).
Pitfalls
- Alpha is a per-model choice. The paper picks alpha on validation data. In the Avg column of the single-task tables in app. O, ViT-B/32 peaks around alpha 0.5 to 0.6 and ViT-L/14 around 0.65 to 0.70. Treat these as examples and choose alpha on your own validation data.
- Interpolation covers every tensor of the saved encoder patch.py. Whatever fine-tuning changes, such as norms or biases, is blended the same way.
- Sequential patching still forgets. Interpolation reduces forgetting but does not remove it §5 §9. Forgetting can compound across steps.
- Parallel patching needs a search. Black-box search for the per-task alphas gave little gain over one shared alpha. The authors' exhaustive search over two tasks showed headroom for better search §5 app. J.
- Small models lose more. The paper reports larger drops on supported tasks for smaller models §9; ViT-B/32 sequential patching without interpolation lost 4.6 points §5.
- Datasets without public test sets. The validation set is used as the test set, and the training set is split stratified app. A. Compare numbers only on the same split.
- Head depends on class names. The head is built from text prompts for each class list. A new class list needs a new head, computed with the same frozen text encoder app. C heads.py.
- Cost. The method adds an interpolation step to standard fine-tuning, and the paper reports no extra cost at fine-tuning or inference time §1. The paper's full experiments used about 10 thousand GPU-hours app. N; it does not give a per-patch figure.
- Typographic data. The synthetic attack data is built from SUN397 with text overlays app. K. The real-world set has 110 images §7 and is released in the repository as a zip file typographic_attack_data.zip.
Lineage
- Builds on: linear weight interpolation for robustness to distribution shift, Wortsman et al. (2021), cited in §1 as the basis for mixing fine-tuned and original weights. No card in this wiki.
- Compared against: early stopping, regularisation towards the initialisation, lower learning rates, EMA, EWC, LwF, re-training with patch data, and objective mixing §4.2 app. E.
- Superseded by: not stated.
Source and license
Adapted from “Patching open-vocabulary models by interpolating weights” by Gabriel Ilharco, Mitchell Wortsman, Samir Yitzhak Gadre, Shuran Song, Hannaneh Hajishirzi, Simon Kornblith, Ali Farhadi, Ludwig Schmidt (arXiv:2208.05592), used under CC BY 4.0. Changes: condensed and restructured for implementation, with tables, equations and algorithms taken from the paper and its code, each cited to its source. Not affiliated with or endorsed by the authors. Code citations refer to https://github.com/mlfoundations/patching at commit 3ca86220b926 (MIT). Any quoted code is shown under that license.