Fine-Tuning Fundamentals

You probably don’t need fine-tuning. Here’s how to tell when you do.

The Fine-Tuning Decision Tree

def decision_tree(task):
    """
    Should you fine-tune? Use this decision tree.
    
    Each question narrows down the best approach.
    """
    print("Fine-tuning decision tree:")
    print(f"  TASK: {task}")
    print()
    
    # Question 1: Does the model already answer this correctly in zero-shot?
    if model_zero_shot(task, accuracy=0.9):
        print("  → Model works. Use zero-shot prompting. Stop here.")
        return "prompting"
    
    # Question 2: Is the task about formatting/style?
    if task.is_formatting_style():
        print("  → Task is about formatting/style. Use few-shot prompting.")
        print("     Fine-tuning won't help (same model, just different formatting).")
        return "few-shot"
    
    # Question 3: Is the task about domain-specific knowledge?
    if task.requires_domain_knowledge():
        print("  → Task needs domain knowledge. Consider fine-tuning.")
        print("  → Better: use RAG (cheaper, easier, no training needed).")
        return "rag_or_finetune"
    
    # Question 4: Is it a unique capability the model doesn't have?
    if task.requires_unique_capability():
        print("  → Unique capability. Fine-tuning IS the right answer.")
        return "fine-tune"

# The key insight from real-world data:
# - 70% of "fine-tuning needed" tasks are solved by prompting alone
# - 20% are solved by RAG
# - 10% actually need fine-tuning

When Fine-Tuning Actually Helps

"""
Fine-tuning helps with:

1. Domain-specific output formatting that the model doesn't know
   - "Return results as JSON with these exact field names"
   - "Write in this specific style/tone"
   - "Always use this template for responses"

2. Teaching the model a pattern it couldn't learn from few-shot alone
   - "Always explain the reasoning before the answer"
   - "Never include citations in responses"
   - "Always include a disclaimer before medical advice"

3. Reducing token usage for repetitive instructions
   - Instead of "Please return JSON with fields A, B, C..." every prompt
   - One-time fine-tune teaches the pattern
   - Saves tokens and improves consistency

Fine-Tuning does NOT help with:
1. Adding knowledge the model doesn't have (use RAG)
2. Improving general capabilities (pre-training is better)
3. Making the model faster (inference cost stays the same)
4. Fixing prompt quality (bad prompts + fine-tuned model = bad results too)

Rule of thumb: if you can solve it with RAG, use RAG.
Fine-tuning is for patterns that RAG can't fix.
"""

Data Requirements for Fine-Tuning

def data_requirements(model_size="7B", task="classification"):
    """
    How much data do you need to fine-tune?
    
    Rule of thumb: more model = more data needed.
    """
    
    requirements = {
        "7B model": {
            "minimum_samples": "~100 high-quality examples",
            "recommended_samples": "~1,000 examples",
            "sweet_spot": "~5,000-10,000 examples",
        },
        "13B model": {
            "minimum_samples": "~500 high-quality examples",
            "recommended_samples": "~5,000 examples",
            "sweet_spot": "~10,000-50,000 examples",
        },
        "70B model": {
            "minimum_samples": "~1,000 high-quality examples",
            "recommended_samples": "~10,000 examples",
            "sweet_spot": "~50,000-100,000 examples",
        },
    }
    
    return requirements.get(model_size, {}).get(task, "Check specific model docs")

# Quality matters more than quantity. 100 carefully curated examples
# can outperform 10,000 random ones. Key quality factors:
# - Examples should match your actual use case
# - No synthetic data (synthetic = training on the same as your prompt)
# - Diversity > quantity (different ways of asking the same question)
# - Clean data (no typos, no noise)
"""

LoRA Parameters Explained

"""
LoRA hyperparameters:

a: the "rank" of the low-rank matrices. Determines model capacity.
- Higher rank = more capacity = better learning but more parameters
- Typical range: 4-128
- Start with rank = 16, increase if loss plateaus

alpha: The LoRA scaling factor. Controls how much the LoRA adapter affects the model.
- Typical: alpha = 2 * rank (standard scaling)
- Alpha / rank determines the effective scaling
- If loss decreases too slowly, increase alpha
- If loss oscillates, decrease alpha

beta1, beta2: Adam optimizer hyperparameters (usually defaults: 0.9, 0.999)

lr: Learning rate. The most important hyperparameter.
- Typical range: 1e-4 to 5e-5 (LOWER than standard training)
- Too high = loss explodes, too low = no improvement
- Cosine annealing schedule works best

epochs: Number of passes through the data.
- Too many = overfitting (very common)
- Too few = underfitting
- Typical: 3-5 epochs with early stopping

batch_size: Number of examples per batch.
- Larger batch = more stable gradients but more memory
- Typical: 4-16 (depending on GPU memory)

warmup_ratio: Fraction of steps for learning rate warmup.
- Typical: 0.03-0.1 (3-10% of total steps)
- Prevents initial gradient explosion

mask_length: For sequence-to-sequence fine-tuning.
- Set to "max" to mask entire sequence
- Use "uniform" for partial masking
"""

Fine-Tuning vs RAG vs Prompting Decision Matrix

"""
When to use what:

| Scenario                          | Best Approach | Why |
|-------------------------------------------------------------|------------------|
| Model already does it well       | Prompting       | No training needed |
| Need different output format     | Few-shot Ppting | Easier than fine-tuning |
| Need domain knowledge not in model | RAG             | Cheaper, no retrained |
| Model keeps ignoring instructions | Fine-tuning     | Teach once, works always |
| Need consistent formatting       | Fine-tuning     | More reliable than prompting |
| Task is common knowledge         | Prompting       | Model already knows |
| Data changes frequently          | RAG             | Update index, retrain model |
| Unique capability needed         | Fine-tuning     | No alternative |
| Need low-latency                 | RAG             | Fine-tuning inference is same speed |
| Need explainability              | RAG             | Can trace to source docs |

Rule of thumb: prompting → RAG → fine-tuning (in that order)
If you can fix it with prompting, don't move to RAG.
If you can fix it with RAG, don't fine-tune.
Fine-tuning is the LAST option, not the first.
"""

Evaluating Fine-Tuning Results

def evaluate_fine_tuning(base_model, fine_tuned_model, eval_dataset):
    """
    Evaluate fine-tuning results.
    
    Comparison: fine-tuned vs base model on the same eval dataset.
    """
    base_accuracy = 0.0
    ft_accuracy = 0.0
    
    for item in eval_dataset:
        base_response = base_model.generate(item.input)
        ft_response = fine_tuned_model.generate(item.input)
        
        if is_correct(base_response, item.answer):
            base_accuracy += 1
        if is_correct(ft_response, item.answer):
            ft_accuracy += 1
    
    base_accuracy /= len(eval_dataset)
    ft_accuracy /= len(eval_dataset)
    
    print(f"Base model accuracy: {base_accuracy:.3f} ({*100:.1f}%)")
    print(f"Fine-tuned accuracy: {ft_accuracy:.3f} ({ft_accuracy*100:.1f}%)")
    print(f"Improvement: {(ft_accuracy - base_accuracy) * 100:.1f}%)
    
    if ft_accuracy - base_accuracy > 0.05:
        print("Fine-tuning helped!")
    else:
        print("Fine-tuning didn't help. Try more data, different hyperparameters, or RAG.

    # Important: also check for catastrophic forgetting
    # If accuracy increases on target task but decreases on unrelated tasks
    # your fine-tune was too aggressive. Use less data, lower lr, or LoRA.
"""

Summary

Fine-tuning is for teaching patterns, not knowledge:

Use the decision tree. Evaluate honestly. Don’t fine-tune unless you have to. When you do: use high-quality data, monitor for overfitting, and compare against the base model.

Fine-tuning is a tool, not a panacea. It’s the last option, not the first.