This project fine-tunes Qwen2-VL-2B-Instruct — a 2-billion-parameter vision-language model — on the ChartQA dataset using QLoRA (4-bit quantisation + Low-Rank Adaptation). The entire pipeline runs on a single NVIDIA T4 GPU (16 GB VRAM).
Given a chart image and a natural-language question, the model produces a short, precise answer.
Input: [bar chart image] + "What is the value for category B in 2022?"
Output: "47.3"
Design Decisions
Decision
Choice
Why
Dataset
ChartQA
Visual QA over charts demands both fine-grained OCR-level visual reading and multi-step numerical/categorical reasoning — a rich multimodal signal that pushes the model to fuse vision and language meaningfully
Model
Qwen2-VL-2B-Instruct
Best open 2B multimodal model as of early 2025; dynamic-resolution ViT encoder handles diverse chart sizes; strong ChartQA baseline before fine-tuning
Fine-tuning method
QLoRA
4-bit NF4 base + FP16 LoRA adapters keeps peak VRAM ≈ 12 GB, well within T4 budget. We get ~95% of full fine-tune quality at ~25% the cost. FP16 (not BF16) is used because T4 is Turing (SM 7.5) and lacks native BF16 tensor cores
LoRA rank
r=16, α=32
r=16 gives sufficient adapter capacity for ChartQA at 2B scale while being 4× faster than r=64. α=2r is a widely validated stable default
LoRA targets
Attention projections only (q/k/v/o_proj)
FFN modules (gate/up/down_proj) doubled compute time on T4 without meaningfully improving ChartQA accuracy at this scale
Batch size
2 × 8 accum = 16 effective
Maximises T4 VRAM utilisation; gradient accumulation recovers the statistical benefit of large batches
Learning rate
2e-4
Standard QLoRA recommendation. Higher than full fine-tune LRs because the frozen base provides a stable anchor
Image resolution cap
256×256 px
Halving each dimension cuts visual tokens by ~4×, the single biggest speed-up on T4. Most chart labels remain readable at this resolution
1from transformers import Qwen2VLForConditionalGeneration, AutoProcessor
2from qwen_vl_utils import process_vision_info
3from PIL import Image
4import torch
56# 1. Pull the merged (full) model — no adapter handling needed7model = Qwen2VLForConditionalGeneration.from_pretrained(8"Yash1608/qwen2vl-2b-chartqa-merged",9 torch_dtype=torch.float16,# FP16: T4 Turing (SM 7.5) has native FP16, no BF16 tensor cores10 device_map="auto",11 trust_remote_code=True,12)13processor = AutoProcessor.from_pretrained(14"Yash1608/qwen2vl-2b-chartqa-merged",15 trust_remote_code=True,16)17model.eval()1819# 2. Prepare input20image = Image.open("chart.png")# your chart image21question ="What is the highest value shown?"2223messages =[{24"role":"user",25"content":[26{"type":"image","image": image},27{"type":"text","text":f"Analyze the chart and answer concisely.\n\nQuestion: {question}"},28],29}]3031# 3. Run inference32text = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)33image_inputs, _ = process_vision_info(messages)34inputs = processor(text=[text], images=[image_inputs], return_tensors="pt").to("cuda")3536with torch.no_grad():37 gen = model.generate(**inputs, max_new_tokens=32, do_sample=False)3839answer = processor.decode(gen[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True)40print("Answer:", answer)
(Alternative) Load base model + LoRA adapters, then merge
python
1from transformers import Qwen2VLForConditionalGeneration, AutoProcessor
2from peft import PeftModel
3import torch
45# Step 1: load base model6base = Qwen2VLForConditionalGeneration.from_pretrained(7"Qwen/Qwen2-VL-2B-Instruct",8 torch_dtype=torch.float16,# FP16 for T4 GPU9 device_map="auto",10 trust_remote_code=True,11)1213# Step 2: attach LoRA adapters14model = PeftModel.from_pretrained(base,"Yash1608/qwen2vl-2b-chartqa")1516# Step 3: merge adapters into base weights (optional but recommended for deployment)17model = model.merge_and_unload()1819# Processor from either the adapter repo or the base model20processor = AutoProcessor.from_pretrained("Yash1608/qwen2vl-2b-chartqa", trust_remote_code=True)
Training Details
Parameter
Value
Base model
Qwen/Qwen2-VL-2B-Instruct
Quantisation
NF4 4-bit + double quantisation
LoRA rank
16
LoRA alpha
32
LoRA dropout
0.05
LoRA targets
q_proj, k_proj, v_proj, o_proj (attention only)
Train samples
6 000
Val samples
300
Epochs
2
Effective batch size
16 (2 × 8 gradient accum.)
Learning rate
2e-4 (cosine decay)
Warmup
5% of steps
Precision
FP16 (fp16=True, bf16=False) — T4 is Turing SM 7.5; BF16 requires Ampere SM 8.0+
Max image size
256 × 256 px
Max sequence length
512 tokens
Hardware
1 × NVIDIA T4 (16 GB)
Evaluation
ChartQA is scored with relaxed accuracy: a prediction is correct if it matches the reference exactly (string match) or within ±5% (for numeric answers).
Split
Relaxed Accuracy
Validation (200 samples)
34.50% (69/200)
Test
—
Reproducing
Clone the repo: git clone https://github.com/pes1ug23am910/NLP_Orange_ChartQA
Open multimodal_finetune_chartqa_Collab_v2_Final.ipynb in Kaggle or Google Colab (T4 runtime)
Set your HF_TOKEN as a secret
Update CFG["hf_repo_id"] with your HuggingFace username
Run all cells top to bottom
Push to HuggingFace Hub
After training completes, use push_to_hub.py to publish your checkpoint:
bash
1# Push both LoRA adapters AND the merged full model (recommended)2python push_to_hub.py --hf_username Yash1608 --repo_name qwen2vl-2b-chartqa
34# Push adapters only (smaller upload, ~100–400 MB)5python push_to_hub.py --hf_username Yash1608 --repo_name qwen2vl-2b-chartqa --skip_merged
67# Push merged full model only8python push_to_hub.py --hf_username Yash1608 --repo_name qwen2vl-2b-chartqa --skip_adapters
Evaluation (Relaxed Accuracy)
ChartQA is scored with relaxed accuracy (exact string match OR numeric match within ±5%).
You can run batch evaluation from the command line:
bash
1# Evaluate merged model on the test split (500 samples)2python inference.py --evaluate --eval_split test --eval_samples 50034# Evaluate via adapter load + merge on the val split5python inference.py --evaluate --use_adapters --eval_split val --eval_samples 200