A single LoRA adapter (405KB) that improves classification performance across all MTEB Classification tasks, built on top of the compressed gomyk/jina-v5-h256-distilled-conv embedding model.
Unlike per-task adapters, this is one universal adapter that enhances the embedding space for classification in general — no task-specific fine-tuning needed at inference time.
Baseline: MTEB default evaluation (logistic regression on frozen embeddings)
+ LoRA: Same evaluation, but with universal LoRA adapter merged into embeddings
Training Method
Multi-Task Classification with shared LoRA backbone:
┌─ Head_Amazon(256→2) ─→ CE loss
├─ Head_Banking(256→77) ─→ CE loss
Input → [EuroBERT ├─ Head_IMDB(256→2) ─→ CE loss
+ LoRA] ├─ Head_MTOP(256→11) ─→ CE loss
→ pool ──►├─ Head_Massive_I(256→60)─→ CE loss
├─ Head_Massive_S(256→18)─→ CE loss
├─ Head_Toxic(256→2) ─→ CE loss
└─ Head_Tweet(256→3) ─→ CE loss
All heads share the same LoRA backbone.
After training, heads are discarded — only the LoRA adapter is kept.
The LoRA-enhanced embeddings are universally better for classification.
Parameter
Value
Training data
112,716 samples from 8 MTEB Classification tasks
Total classes
175 (across all tasks)
Optimizer
AdamW (lr=2e-4, weight_decay=0.01)
Scheduler
CosineAnnealingLR
Epochs
10
Batch size
32
Max sequence length
128
Gradient clipping
max_norm=1.0
Loss function
Cross-Entropy (per task head)
Final training loss
0.189
Final training accuracy
~93%
Usage
Option 1: Merge LoRA into base model
python
1import torch
2import torch.nn as nn
3from transformers import AutoModel, AutoTokenizer
4from huggingface_hub import hf_hub_download
56# Load base model7model = AutoModel.from_pretrained(8"gomyk/jina-v5-h256-distilled-conv", trust_remote_code=True)9tokenizer = AutoTokenizer.from_pretrained(10"gomyk/jina-v5-h256-distilled-conv", trust_remote_code=True)1112# Download and merge LoRA13lora_path = hf_hub_download(14"gomyk/jina-v5-h256-lora-classification-universal",15"lora_adapter.pt")16lora_state = torch.load(lora_path, map_location="cpu", weights_only=True)1718idx =019for name, module in model.named_modules():20for target in["q_proj","k_proj","v_proj","o_proj"]:21 child =getattr(module, target,None)22if child isnotNoneandisinstance(child, nn.Linear):23 A = lora_state[f"lora_{idx}_A"]24 B = lora_state[f"lora_{idx}_B"]25 scaling = lora_state[f"lora_{idx}_scaling"].item()26 child.weight.data +=(scaling *(A @ B)).T
27 idx +=12829# Now use as normal embedding model30model.eval()31inputs = tokenizer("This movie was great!", return_tensors="pt",32 truncation=True, max_length=128)33with torch.no_grad():34 outputs = model(**inputs)35 hidden = outputs.last_hidden_state
36 mask = inputs["attention_mask"].unsqueeze(-1).float()37 embedding =(hidden * mask).sum(1)/ mask.sum(1).clamp(min=1e-9)3839print(embedding.shape)# [1, 256]
Option 2: Use with downstream classifier
python
1# After merging LoRA (see above), add your own classifier2from sklearn.linear_model import LogisticRegression
34# Encode your training data5train_embeddings =[]# encode your texts with the merged model6train_labels =[...]78clf = LogisticRegression(max_iter=1000)9clf.fit(train_embeddings, train_labels)1011# Predict12test_embedding =...# encode test text13prediction = clf.predict(test_embedding)
File Structure
.
├── README.md # This file
├── lora_adapter.pt # LoRA A/B matrices (405KB)
├── meta.json # Training metadata
└── task_info.json # Per-task class counts
How LoRA Works
For each of the 24 attention projections (4 per layer x 6 layers):
Original: y = W @ x W: [256, 256] — frozen
With LoRA: y = W @ x + (16/8) * (x @ A) @ B
A: [256, 8] — learned down-projection
B: [8, 256] — learned up-projection
At merge time: W_new = W + 2.0 * (A @ B)^T — zero overhead at inference