File size: 5,706 Bytes
6b92ad3 39a970e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | ---
license: apache-2.0
---
---
license: apache-2.0
base_model: Qwen/Qwen2.5-3B
tags:
- qwen2
- java
- qlora
- domain-adaptation
- code
- fine-tuned
language:
- en
library_name: transformers
---
# JavaExpert-Qwen2.5-3B
**Domain-locked Java QA via QLoRA on consumer hardware.**
Fine-tuned from `Qwen/Qwen2.5-3B` on 7,921 Java QA pairs using QLoRA β trained entirely on a single NVIDIA RTX 5050 (8 GB VRAM), no cloud, no A100s. The model answers Java questions accurately and refuses everything else, without external guardrails.
## Evaluation Results
| Metric | Target | Achieved |
|--------|--------|----------|
| Validation Perplexity | < 10 | **2.40** |
| Peak Training VRAM | β€ 8 GB | **< 7 GB** |
| Inference VRAM | β | **< 2 GB** |
| Java Correctness | β | **8.5 / 10** |
| Domain Restriction | β | **8.5 / 10** |
| Hallucination Control | β | **8.0 / 10** |
## Quick Start
```python
from transformers import AutoModelForCausalLM, AutoTokenizer
model = AutoModelForCausalLM.from_pretrained("Debarun12/JavaExpert-Qwen2.5-3B")
tokenizer = AutoTokenizer.from_pretrained("Debarun12/JavaExpert-Qwen2.5-3B")
messages = [
{"role": "system", "content": "You are JavaExpert, a Java programming assistant."},
{"role": "user", "content": "What is the difference between HashMap and LinkedHashMap in Java?"}
]
input_ids = tokenizer.apply_chat_template(messages, return_tensors="pt")
output = model.generate(input_ids, max_new_tokens=300)
print(tokenizer.decode(output[0], skip_special_tokens=True))
```
## Domain Restriction β What It Does
The model was trained with explicit negative examples (unanswerable/out-of-domain questions), so it refuses non-Java queries without any post-processing filter:
| Query | Response |
|-------|----------|
| What is the capital of France? | Refuses cleanly |
| Who won the FIFA World Cup? | Refuses cleanly |
| How do I train a neural network in Python? | Partial refusal (known limitation β see below) |
## Training Details
**Base model:** `Qwen/Qwen2.5-3B` β selected over the 7B (OOM risk on 8 GB) and 1.5B (higher hallucination tendency).
**Method:** QLoRA β base model quantized to 4-bit NF4 (~1.9 GB), adapters trained in bf16.
**Hardware:** NVIDIA GeForce RTX 5050, 8 GB VRAM. Peak usage < 7 GB.
### Memory Optimisation Decisions
Every config choice was driven by measured VRAM impact:
| Component | Initial | Final | VRAM Saved |
|-----------|---------|-------|------------|
| LoRA rank | 32 across 7 modules | 16 on q_proj, v_proj | β1.5 GB |
| Batch size | 4 | 1 + grad accum Γ8 | β2.0 GB |
| Optimizer | AdamW | Adafactor | β1.0 GB |
| Compute dtype | fp16 | bf16 | Stable on Blackwell |
### Hyperparameters
```
per_device_train_batch_size = 1
gradient_accumulation_steps = 8 # effective batch = 8
optim = "adafactor"
bf16 = True
learning_rate = 2e-4
num_train_epochs = 3
lora_r = 16
lora_target_modules = ["q_proj", "v_proj"]
```
### Training Loss
| Epoch | Train Loss | Val Loss | Val Perplexity |
|-------|-----------|----------|----------------|
| 1 | 0.8511 | 0.9343 | 2.55 |
| 2 | 0.8367 | 0.8839 | 2.42 |
| 3 | 0.7780 | 0.8772 | **2.40** |
Validation loss decreased monotonically. No overfitting across 3 epochs β the train/val gap remained narrow and stable throughout.
## Dataset
Built from scratch β not an off-the-shelf dataset:
- **Source:** 42,000+ lines of Java documentation, extracted from PDFs
- **Pipeline:** Custom preprocessing β sliding-window chunking (150 words, 30-word overlap) β LLM-assisted QA generation via `qwen2.5:7b`
- **QA pairs:** 7,921 chat-formatted (system + user + assistant)
- **Question types:** 36% definitional / 32% procedural / 32% reasoning + explicit unanswerable/refusal examples
- **Estimated cleanliness:** 95%+
Dataset: [Debarun12/JavaExpert-Qwen2.5-3B-DATASET](https://huggingface.co/datasets/Debarun12/JavaExpert-Qwen2.5-3B-DATASET)
## Bugs Found and Fixed During Training
**SFTTrainer step-count explosion** β `packing=True` with pre-tokenized input caused 2,139 steps on 90 samples (~24Γ expected). Root cause: SFTTrainer misinterprets packed sequence lengths. Fix: replaced with standard `Trainer` + `DataCollatorForLanguageModeling`. Steps normalized to 2,673 across 3 epochs.
**PyTorch 2.6 checkpoint incompatibility** β `weights_only=True` default change raised `UnpicklingError` on resume. Fix: automated removal of `rng_state.pth` from checkpoint directories before resumption.
## Limitations
- **Domain-adjacent leakage:** Python and general algorithm queries sometimes receive partial responses rather than clean refusals. The boundary between the Java corpus and the 3B's pretraining knowledge on general programming topics is not perfectly sharp.
- **Corpus grounding:** The model answers Java questions correctly but draws on pretraining generalisation rather than reproducing specific training corpus facts β a fundamental limitation of generative fine-tuning without retrieval.
**Recommended next steps:** RAG layer over the source corpus for strict factual grounding; DPO with curated refusal preference pairs to sharpen the domain boundary on adjacent technical topics.
## Production Deployment
LoRA adapters were merged into base weights via `merge_and_unload()` β this is a single self-contained checkpoint with no adapter dependency at runtime. Inference runs at under 2 GB VRAM.
## Repository
Training notebooks and dataset generation pipeline: [GitHub β debarun23](https://github.com/debarun23)
---
*Trained on NVIDIA GeForce RTX 5050 (8 GB VRAM) Β· June 2026 Β· Author: Debarun Das* |