English | 中文
Leveraging Qwen3-VL-32B's reasoning capability to construct an augmented dataset via knowledge distillation, then fine-tuning Qwen3-VL-2B with QLoRA to achieve near-large-model chart numerical reasoning under single-GPU deployable conditions.
ChartQA/
├── sft/ # Supervised Fine-Tuning
│ ├── __init__.py
│ ├── config.py # Hyperparameter configuration (dataclass system)
│ ├── model.py # Model loading and LoRA adaptation
│ ├── data_loader.py # Dataset loading and preprocessing
│ ├── collator.py # Data collation and label masking
│ ├── callbacks.py # Training callbacks (monitoring, early stopping)
│ ├── trainer.py # Trainer setup
│ └── train.py # SFT training entry point
│
├── ChartQADataset/ # ChartQA dataset
│ ├── train/ # Training split
│ ├── val/ # Validation split
│ └── test/ # Test split
│
├── test_model.py # Evaluation script (six metrics)
├── requirements.txt # Python dependencies
├── README_en.md # This document
└── LICENSE # MIT License
pip install -r requirements.txt| Dimension | Qwen3-VL-32B-Thinking | Qwen3-VL-8B-Thinking | Qwen3-VL-2B-Thinking | Gap |
|---|---|---|---|---|
| FP16 VRAM (weights only) | ~64 GB | ~16 GB | ~4 GB | 16x (32B vs 2B) |
| Minimum Deployment Hardware | Multiple A100 / H100 | Single 5090 32GB | Single RTX 4060 8GB | 10-30x hardware cost difference |
| Single Query Latency | ~2-5 seconds | ~0.8-1.5 seconds | ~0.3-0.8 seconds | 3-6x |
Contradiction 1: Deployment Cost vs Reasoning Ability
→ 32B performs well but is too expensive; 2B is affordable but lacks numerical reasoning
Contradiction 2: Data Annotation vs Annotation Cost
→ To teach 2B reasoning chains, we need (question, reasoning_chain, answer) triplets,
but manually writing tens of thousands of structured reasoning chains is prohibitive
Contradiction 3: End-to-End SFT vs Reasoning Ability
→ If we only use (question, answer) pairs for SFT, the small model degenerates into
a "guessing" mode — seeing "2019" and outputting the tallest bar's value,
rather than truly learning to read axes, extract values, and compute
Solution: Use 32B large model's reasoning output to generate training annotations for the 2B small model, teaching it to "think step-by-step like a large model" and achieve near-large-model numerical reasoning accuracy under single-GPU deployable model size.
The original ChartQA dataset contains only (image, question, label) three fields, without reasoning chain annotations:
{
"image": "multi_col_20134.png",
"question": "What is the estimated cyber security market size of the defense and intelligence sector in 2017?",
"label": "250"
}Using Qwen3-VL-32B-Thinking as the teacher model, we perform item-by-item reasoning on the ChartQA dataset to automatically generate high-quality reasoning chains. The teacher model's output format is:
Inpy prompt template:
You are a helpful assistant capable of visual reasoning for answering questions about charts.
Given a chart image and a question, you will first analyze the chart step by step, then provide the final answer in a JSON format:
{answer: "your_answer"}
Make sure to include a detailed reasoning process before the final answer.
Here is the chart image: {image_path}
Question: {question}
Output format:
<think>reasoning content</think>
<answer>answer</answer>
Example:
input:
"""
You are a helpful assistant capable of visual reasoning for answering questions about charts.
Given a chart image and a question, you will first analyze the chart step by step, then provide the final answer in a JSON format:
{answer: "your_answer"}
Make sure to include a detailed reasoning process before the final answer.
Here is the chart image: multi_col_20134.png
Question: What is the estimated cyber security market size of the defense and intelligence sector in 2017?
"""<think>Got it, let's look at the bar chart. The question is about the defense and intelligence sector's cyber security market size in 2017. The chart has two segments for each year: blue is "Other Public Sector" and black is "Defence and Intelligence".
For 2017, the black segment (defence and intelligence) shows a value. Let me check the numbers. The black part for 2017 is labeled as 250 million GBP. Let me confirm. The y-axis is value in million GBP. The black bar in 2017 reaches up to 250. Yes, the label on top of the black segment for 2017 is 250.</think>
<answer>250</answer>
The distilled _augmented.json is parsed and split into _converted.json (5-field format) to adapt to the SFT training code:
{
"image": "multi_col_20134.png",
"question": " What is the estimated cyber security market size of the defense and intelligence sector in 2017?",
"think": "Got it, let's look at the bar chart. The question is about the defense and intelligence sector's cyber security market size in 2017. The chart has two segments for each year: blue is \"Other Public Sector\" and black is \"Defence and Intelligence\".\nFor 2017, the black segment (defence and intelligence) shows a value. Let me check the numbers. The black part for 2017 is labeled as 250 million GBP. Let me confirm. The y-axis is value in million GBP. The black bar in 2017 reaches up to 250. Yes, the label on top of the black segment for 2017 is 250.",
"answer": "250",
"label": "250"
}Key Design Decisions:
thinkfield: The step-by-step reasoning chain generated by the teacher model, serving as a supervision signal to teach the model "analyze chart structure → locate data → compute → give answer"answerfield: The teacher model's output answer (not directly used during training)labelfield: The ground truth from the original dataset; during training, the model's target answer uses the real label rather than the teacher model's output, ensuring dual supervision signals where the reasoning chain is a reference and the answer is the standard
ChartQADataset/
├── train/
│ ├── train_augmented.json # Raw distillation output (with <think> tags)
│ ├── train_converted.json # Converted training data (5-field format)
│ └── png/ # Chart images
├── val/
│ ├── val_augmented.json
│ ├── val_converted.json
│ └── png/
└── test/
├── test_augmented.json
└── png/
- Base Model: Qwen3-VL-2B-Thinking (vision-language model with CoT reasoning capability)
- Fine-tuning Method: QLoRA (Quantized Low-Rank Adaptation)
- Trainable Parameters: ~1-2% (only training attention layer projection matrices)
LoRA injects trainable rank decomposition matrices into the pre-trained model's weight matrices through low-rank decomposition, freezing the original model parameters and only training the low-rank adapters:
W = W₀ + BA
Where W₀ is the frozen pre-trained weight, and B and A are trainable low-rank matrices (r << d).
QLoRA further quantizes the base model to 4-bit, significantly reducing VRAM usage while maintaining accuracy.
| Parameter | Value | Description |
|---|---|---|
r (rank) |
16 | Rank of low-rank matrices, controls trainable parameters |
lora_alpha |
32 | Scaling factor, typically set to 2r |
target_modules |
q_proj, k_proj, v_proj, o_proj |
4 projection matrices in attention layers |
lora_dropout |
0.05 | Prevents overfitting |
bias |
none |
Do not train bias terms |
task_type |
CAUSAL_LM |
Causal language modeling task |
| Parameter | Value |
|---|---|
| Learning rate | 2e-5 |
| Batch size | 2 (per device) |
| Gradient accumulation | 8 steps |
| Effective batch size | 16 |
| Max sequence length | 1024 |
| Precision | BF16 |
| Optimizer | adamw_torch |
| NEFTune noise alpha | 5.0 (increases input robustness) |
| Early Stopping patience | 3 |
Raw JSON → Load data → Build conversation → Apply template → Processor encoding → Label masking → Training batch
- Load data: Read 5-field format data from
train_converted.json - Build conversation: Combine
(system_prompt, question, think, label)into a three-turn dialogue - Apply template: Convert dialogue to full text sequence using
apply_chat_template - Processor encoding: Images go through the visual encoder, text through the tokenizer
- Label masking: Use
_mask_labelsto mask the part before<think>as-100, training only the model to generate reasoning chains and answers - Training batch: Dynamically pad to batch maximum length
Training sample labels are formatted into a unified template:
{think_text} {answer: "label"}
The system prompt explicitly requires the model to "perform step-by-step reasoning, then output the final answer in JSON format: {answer: 'your_answer'}".
cd sft
python train.py| Metric | Description |
|---|---|
| Exact Match | Normalized prediction matches ground truth exactly |
| Numeric Accuracy | Tolerance-based numerical comparison (relative error < 0.1%) |
| Token F1 | Token-level F1 score between prediction and ground truth |
| Format Compliance | Percentage of outputs containing {answer: '...'} JSON format |
| Answer Extraction Rate | Percentage of successfully extracted JSON answers from model outputs |
| Avg Response Length | Average token count of model outputs |
python test_model.pyResults are saved as JSON files for easy cross-model version comparison.
The core comparison in this project is between the distilled + QLoRA fine-tuned 2B model and the original un-fine-tuned 8B base model:
| Metric | Qwen3-VL-2B (Distilled + QLoRA SFT) | Qwen3-VL-8B-Thinking (Base) | Description |
|---|---|---|---|
| Exact Match | 90.1% (64/71) | 90.1% (64/71) | Tied |
| Numeric Accuracy | 93.0% (53/57) | 93.0% (53/57) | Comparable numerical reasoning |
| Format Compliance | 100% (71/71) | 100% (71/71) | Both consistently output JSON |
| Answer Extraction Rate | 100% (71/71) | 98.6% (70/71) | 2B has more stable formatting |
| Mean Token F1 | 0.901 | 0.901 | Identical answer token overlap |
Key Findings:
- The distilled + QLoRA fine-tuned 2B model ties with the original 8B model in Exact Match (90.1% vs 90.1%)
- Numerical reasoning accuracy is identical (93.0% vs 93.0%), and Token F1 is exactly the same (0.901)
- Yet the 2B model's VRAM requirement is only 1/4 of 8B's, reducing deployment cost by an order of magnitude
- The 2B model's inference latency is 2-3x lower than 8B's, making it more suitable for high-concurrency scenarios
- Significant cost-performance advantage: Using 1/4 of the VRAM and 1/3 of the inference time to achieve the same reasoning accuracy as the 8B base model