🧭 Roadmap
Difficulty⭐⭐⭐⭐ / 5 ⏱ Read time5-6 hours fine-tuningtrainingintermediate
🔬
MiniLLM Lab · Stage 4 — Train the Model
Fine-tuning is what transforms a pretrained base into a task-specific model — the key technique in Stage 4.
View project →
let navbar = [];
let loadingMessage = dv.el("span", "**Loading navigation...**", {attr: {style: "font-size:13px; color:gray"}});

let allPages = dv.pages("#dashboard").sort(page => page.file.folder, "asc");
let filteredPages = allPages.filter(p => 
    p.file.tags.values.includes("#dashboard") && p?.include_in_navbar == true
);

for(let page of filteredPages){
    let navItem = '';
    let navName = 'Untitled';
    let navLink = '';

    if(page.navbar_name === undefined){
        navName = page.file.name;
    } else {
        navName = page.navbar_name;
    }
    navLink = page.file.path;

    // Format the nav  item link
    if(dv.current().file.path === page.file.path){
        navItem = `**[[${navLink}|${navName}]]**`
    } else {
        navItem = `[[${navLink}|${navName}]]`
    }

    navbar.push(navItem)
}

dv.paragraph(navbar.join(' | '))

if(filteredPages.values.length > 0){
    loadingMessage.remove();
}

🎯 Fine-tuning

🌟 Start With a Real-World Scenario

Imagine you're building a medical AI assistant:

Using ChatGPT (General Model):

You: "What's the treatment for hypertension?"
ChatGPT: "Hypertension can be treated with lifestyle changes and medications..."

✅ Correct, but generic

You: "What's the ICD-10 code for essential hypertension?"
ChatGPT: "I believe it's I10, but please verify..."

❌ Uncertain, not confident

You: "Draft a prescription for lisinopril 10mg"
ChatGPT: "I cannot provide medical prescriptions..."

❌ Refuses (too cautious)

Problem: ChatGPT is a generalist. It knows a bit about everything but isn't an expert in YOUR domain!


Using Fine-Tuned Medical Model:

You: "What's the treatment for hypertension?"
MedicalGPT: "For stage 1 hypertension (130-139/80-89 mmHg), initiate with:
1. Lifestyle modifications (DASH diet, exercise)
2. If no improvement in 3 months, start ACE inhibitor or ARB
3. Monitor BP weekly, adjust dosage as needed
Reference: 2023 AHA/ACC Guidelines"

✅ Specific, confident, with references!

You: "What's the ICD-10 code for essential hypertension?"
MedicalGPT: "I10 - Essential (primary) hypertension"

✅ Instant, accurate!

You: "Draft a prescription for lisinopril 10mg"
MedicalGPT: "Lisinopril 10mg PO daily
Indication: Essential hypertension
Monitoring: Check BP in 2 weeks, renal function in 1 month
Contraindications: Pregnancy, bilateral renal artery stenosis"

✅ Professional, detailed!


The Difference? Fine-Tuning!

You took GPT-3.5 and trained it on:

  • 50,000 medical Q&A pairs
  • 10,000 clinical notes
  • 5,000 prescription examples
  • Medical textbooks and guidelines

Result: A specialized medical AI that:

  • Knows medical terminology perfectly
  • Follows clinical guidelines
  • Provides confident, accurate answers
  • Understands your specific workflow

Real-World Examples:

GitHub Copilot:

  • Base model: GPT (general code)
  • Fine-tuned on: Billions of lines of GitHub code
  • Result: Writes code in your style, knows your frameworks

Jasper AI (Content Writing):

  • Base model: GPT-3
  • Fine-tuned on: Marketing copy, blog posts, ads
  • Result: Writes in specific brand voices, follows marketing best practices

Bloomberg GPT (Finance):

  • Base model: GPT
  • Fine-tuned on: Financial news, reports, market data
  • Result: Understands financial jargon, analyzes markets

Your Company's Customer Support Bot:

  • Base model: GPT-3.5
  • Fine-tuned on: Your product docs, support tickets, FAQs
  • Result: Answers questions about YOUR product specifically

The Economics:

Option 1: Use ChatGPT API (No fine-tuning)

  • Cost: $0.002 per 1K tokens
  • Quality: 70% accuracy on your domain
  • Speed: Fast
  • Problem: Generic answers, frequent mistakes

Option 2: Fine-tune GPT-3.5

  • Training cost: $200 (one-time)
  • Inference cost: $0.012 per 1K tokens (6x more expensive)
  • Quality: 95% accuracy on your domain
  • Speed: Same
  • Benefit: Specialized, accurate, confident

When to fine-tune:

  • ✅ You have 500+ high-quality examples
  • ✅ Your domain is specialized (medical, legal, finance)
  • ✅ You need consistent output format
  • ✅ Generic models make too many mistakes
  • ✅ You're making millions of API calls (cost savings)

When NOT to fine-tune:

  • ❌ You have <100 examples
  • ❌ Your task is general (summarization, translation)
  • ❌ Prompt engineering works well enough
  • ❌ You're just prototyping

By the end of this guide, you'll understand:

  • How to fine-tune GPT, BERT, and other models
  • When fine-tuning is worth the cost vs. prompt engineering
  • How to prepare training data (quality > quantity!)
  • LoRA and other efficient fine-tuning techniques
  • How to avoid catastrophic forgetting
  • How to evaluate fine-tuned models
  • How to deploy fine-tuned models in production

The power: Fine-tuning turns a jack-of-all-trades into a master of YOUR trade! 🎯

Let's dive in! 🚀


🗺️ Concept Mind Map

                        FINE-TUNING
                            |
        +-------------------+-------------------+
        |                   |                   |
    APPROACHES          TECHNIQUES         USE CASES
        |                   |                   |
        |                   |                   |
    Full Fine-Tuning    LoRA               Domain Adaptation
    - Update all        - Low-Rank         - Medical AI
      parameters          Adaptation       - Legal AI
    - High cost         - 0.1% params      - Finance AI
    - Best quality      - 90% cost saving  - Code generation
    - Needs GPU         - Same quality
        |                   |                   |
        |                   |                   |
    Partial Fine-Tuning QLoRA              Task Specialization
    - Freeze early      - Quantized LoRA   - Sentiment analysis
      layers            - 4-bit precision  - Named entity
    - Update last       - Even cheaper       recognition
      layers            - Runs on CPU      - Text classification
    - Medium cost       - Great for edge   - Question answering
        |                   |                   |
        |                   |                   |
    Adapter Layers      Prompt Tuning      Style Transfer
    - Add small         - Optimize soft    - Brand voice
      modules             prompts          - Writing style
    - Keep base         - No model         - Tone adjustment
      frozen              changes          - Format consistency
    - Very efficient    - Ultra cheap

Fine-Tuning vs Alternatives:

APPROACH          COST    QUALITY   FLEXIBILITY   WHEN TO USE
Prompting         $       ⭐⭐      ⭐⭐⭐⭐⭐    <100 examples
Few-Shot          $       ⭐⭐⭐    ⭐⭐⭐⭐      100-500 examples
Fine-Tuning       $$$     ⭐⭐⭐⭐⭐  ⭐⭐         1000+ examples
LoRA              $$      ⭐⭐⭐⭐⭐  ⭐⭐⭐       1000+ examples

Training Pipeline:

STEP 1: Prepare Data
Training Examples (1000+)
    ↓
Format: {"prompt": "...", "completion": "..."}
    ↓
Split: Train (80%) / Val (10%) / Test (10%)
    ↓
STEP 2: Choose Method
Full Fine-Tune vs LoRA vs QLoRA
    ↓
STEP 3: Train
Epochs: 3-5
Learning Rate: 2e-5 (low!)
Batch Size: 8-16
    ↓
STEP 4: Evaluate
Accuracy, F1, BLEU, ROUGE
    ↓
STEP 5: Deploy
Save model → API → Production

Key Considerations:

  • Data Quality > Quantity: 500 good examples beat 5000 bad ones
  • Learning Rate: Too high = catastrophic forgetting
  • Overfitting: Use early stopping and validation set
  • Cost: LoRA is 90% cheaper than full fine-tuning

🧠 Core Concept

What is Fine-tuning?

Fine-tuning continues training a pre-trained model on your specific dataset:

# Pre-training (done by OpenAI, Anthropic, etc.)
model = train_on_massive_dataset(trillions_of_tokens)

# Fine-tuning (what you do)
specialized_model = fine_tune(model, your_specific_data)

When to Fine-tune vs Prompt

graph TD
    A[Need AI for Task] --> B{Have Examples?}
    B -->|Few| C[Use Prompting]
    B -->|Many 1000+| D{Need Consistency?}
    D -->|Yes| E[Fine-tune]
    D -->|No| C
    E --> F{Have Budget?}
    F -->|Yes| G[Full Fine-tune]
    F -->|Limited| H[Use LoRA]
    
    style E fill:#4CAF50
    style G fill:#2196F3
    style H fill:#FF9800

Use Prompting When:

  • You have < 100 examples
  • Task changes frequently
  • Need flexibility
  • Limited budget

Use Fine-tuning When:

  • You have 1000+ examples
  • Need consistent behavior
  • Domain-specific terminology
  • Want to reduce prompt length
  • Need faster inference

💻 Code Examples

Example 1: Fine-tune BERT for Classification

from transformers import BertForSequenceClassification, BertTokenizer, Trainer, TrainingArguments
from datasets import load_dataset
import torch

# Load pre-trained BERT
model_name = "bert-base-uncased"
model = BertForSequenceClassification.from_pretrained(model_name, num_labels=2)
tokenizer = BertTokenizer.from_pretrained(model_name)

# Prepare your dataset
def prepare_data(examples):
    """Tokenize and prepare data for training."""
    return tokenizer(
        examples['text'],
        padding='max_length',
        truncation=True,
        max_length=128
    )

# Load and prepare dataset
dataset = load_dataset('imdb')  # Example: sentiment analysis
tokenized_dataset = dataset.map(prepare_data, batched=True)

# Set up training arguments
training_args = TrainingArguments(
    output_dir='./results',
    num_train_epochs=3,
    per_device_train_batch_size=16,
    per_device_eval_batch_size=64,
    warmup_steps=500,
    weight_decay=0.01,
    logging_dir='./logs',
    logging_steps=100,
    evaluation_strategy="epoch",
    save_strategy="epoch",
    load_best_model_at_end=True,
)

# Create Trainer
trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=tokenized_dataset['train'],
    eval_dataset=tokenized_dataset['test'],
)

# Fine-tune the model
print("Starting fine-tuning...")
trainer.train()

# Evaluate
results = trainer.evaluate()
print(f"Evaluation results: {results}")

# Save the fine-tuned model
model.save_pretrained('./fine_tuned_bert')
tokenizer.save_pretrained('./fine_tuned_bert')

# Use the fine-tuned model
def predict(text):
    inputs = tokenizer(text, return_tensors='pt', padding=True, truncation=True)
    outputs = model(**inputs)
    prediction = torch.argmax(outputs.logits, dim=1)
    return "Positive" if prediction == 1 else "Negative"

# Test
print(predict("This movie was amazing!"))  # Positive
print(predict("Terrible waste of time."))  # Negative

Example 2: Fine-tune GPT for Text Generation

from transformers import GPT2LMHeadModel, GPT2Tokenizer, TextDataset, DataCollatorForLanguageModeling
from transformers import Trainer, TrainingArguments

# Load pre-trained GPT-2
model_name = "gpt2"
model = GPT2LMHeadModel.from_pretrained(model_name)
tokenizer = GPT2Tokenizer.from_pretrained(model_name)

# Add padding token
tokenizer.pad_token = tokenizer.eos_token

# Prepare your custom dataset
def create_dataset(file_path, tokenizer, block_size=128):
    """Create dataset from text file."""
    dataset = TextDataset(
        tokenizer=tokenizer,
        file_path=file_path,
        block_size=block_size
    )
    return dataset

# Data collator for language modeling
data_collator = DataCollatorForLanguageModeling(
    tokenizer=tokenizer,
    mlm=False  # GPT uses causal LM, not masked LM
)

# Training arguments
training_args = TrainingArguments(
    output_dir='./gpt2_finetuned',
    overwrite_output_dir=True,
    num_train_epochs=3,
    per_device_train_batch_size=4,
    save_steps=1000,
    save_total_limit=2,
    prediction_loss_only=True,
    learning_rate=5e-5,
)

# Create dataset (you need a text file with your training data)
train_dataset = create_dataset('./your_training_data.txt', tokenizer)

# Create Trainer
trainer = Trainer(
    model=model,
    args=training_args,
    data_collator=data_collator,
    train_dataset=train_dataset,
)

# Fine-tune
print("Fine-tuning GPT-2...")
trainer.train()

# Save model
model.save_pretrained('./gpt2_finetuned')
tokenizer.save_pretrained('./gpt2_finetuned')

# Generate text with fine-tuned model
def generate_text(prompt, max_length=100):
    inputs = tokenizer.encode(prompt, return_tensors='pt')
    outputs = model.generate(
        inputs,
        max_length=max_length,
        num_return_sequences=1,
        no_repeat_ngram_size=2,
        temperature=0.7,
        top_p=0.9,
    )
    return tokenizer.decode(outputs[0], skip_special_tokens=True)

# Test
print(generate_text("Once upon a time"))

Example 3: LoRA Efficient Fine-tuning

from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import LoraConfig, get_peft_model, TaskType
import torch

# Load base model
model_name = "gpt2"
model = AutoModelForCausalLM.from_pretrained(model_name)
tokenizer = AutoTokenizer.from_pretrained(model_name)

# Configure LoRA
lora_config = LoraConfig(
    task_type=TaskType.CAUSAL_LM,
    r=8,  # Rank of update matrices
    lora_alpha=32,  # Scaling factor
    lora_dropout=0.1,
    target_modules=["c_attn"],  # Which layers to apply LoRA to
)

# Apply LoRA to model
model = get_peft_model(model, lora_config)

# Print trainable parameters
model.print_trainable_parameters()
# Output: trainable params: 294,912 || all params: 124,734,720 || trainable%: 0.24%

# Training setup (same as before)
training_args = TrainingArguments(
    output_dir='./lora_finetuned',
    num_train_epochs=3,
    per_device_train_batch_size=8,
    learning_rate=3e-4,  # Higher LR for LoRA
    logging_steps=100,
)

# Train (much faster and uses less memory!)
trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=train_dataset,
)

trainer.train()

# Save LoRA weights (only ~1-2MB instead of full model!)
model.save_pretrained('./lora_weights')

# Load LoRA weights later
from peft import PeftModel

base_model = AutoModelForCausalLM.from_pretrained(model_name)
lora_model = PeftModel.from_pretrained(base_model, './lora_weights')

# Use the model
def generate_with_lora(prompt):
    inputs = tokenizer(prompt, return_tensors='pt')
    outputs = lora_model.generate(**inputs, max_length=100)
    return tokenizer.decode(outputs[0])

print(generate_with_lora("Hello, I am"))

Example 4: Evaluation and Metrics

from sklearn.metrics import accuracy_score, precision_recall_fscore_support, confusion_matrix
import numpy as np

class ModelEvaluator:
    """Comprehensive model evaluation."""
    
    def __init__(self, model, tokenizer):
        self.model = model
        self.tokenizer = tokenizer
    
    def evaluate_classification(self, test_data, test_labels):
        """Evaluate classification model."""
        predictions = []
        
        for text in test_data:
            inputs = self.tokenizer(text, return_tensors='pt', padding=True, truncation=True)
            outputs = self.model(**inputs)
            pred = torch.argmax(outputs.logits, dim=1).item()
            predictions.append(pred)
        
        # Calculate metrics
        accuracy = accuracy_score(test_labels, predictions)
        precision, recall, f1, _ = precision_recall_fscore_support(
            test_labels, predictions, average='weighted'
        )
        
        # Confusion matrix
        cm = confusion_matrix(test_labels, predictions)
        
        results = {
            'accuracy': accuracy,
            'precision': precision,
            'recall': recall,
            'f1': f1,
            'confusion_matrix': cm
        }
        
        return results
    
    def evaluate_generation(self, prompts, references):
        """Evaluate text generation quality."""
        from rouge import Rouge
        from nltk.translate.bleu_score import sentence_bleu
        
        rouge = Rouge()
        bleu_scores = []
        rouge_scores = []
        
        for prompt, reference in zip(prompts, references):
            # Generate
            generated = self.generate(prompt)
            
            # BLEU score
            bleu = sentence_bleu([reference.split()], generated.split())
            bleu_scores.append(bleu)
            
            # ROUGE score
            scores = rouge.get_scores(generated, reference)[0]
            rouge_scores.append(scores)
        
        return {
            'avg_bleu': np.mean(bleu_scores),
            'avg_rouge_1': np.mean([s['rouge-1']['f'] for s in rouge_scores]),
            'avg_rouge_2': np.mean([s['rouge-2']['f'] for s in rouge_scores]),
            'avg_rouge_l': np.mean([s['rouge-l']['f'] for s in rouge_scores]),
        }
    
    def generate(self, prompt, max_length=100):
        """Generate text."""
        inputs = self.tokenizer(prompt, return_tensors='pt')
        outputs = self.model.generate(**inputs, max_length=max_length)
        return self.tokenizer.decode(outputs[0], skip_special_tokens=True)

# Example usage
evaluator = ModelEvaluator(model, tokenizer)

# Evaluate classification
test_texts = ["Great movie!", "Terrible film."]
test_labels = [1, 0]
results = evaluator.evaluate_classification(test_texts, test_labels)

print(f"Accuracy: {results['accuracy']:.4f}")
print(f"F1 Score: {results['f1']:.4f}")
print(f"Confusion Matrix:\n{results['confusion_matrix']}")

Example 5: Production Fine-tuning Pipeline

import wandb
from transformers import TrainerCallback
import json

class ProductionFineTuner:
    """Complete production fine-tuning pipeline."""
    
    def __init__(self, model_name, task_type='classification'):
        self.model_name = model_name
        self.task_type = task_type
        self.model = None
        self.tokenizer = None
        
        # Initialize Weights & Biases for tracking
        wandb.init(project="fine-tuning", name=f"{model_name}_{task_type}")
    
    def prepare_data(self, train_file, val_file, test_file):
        """Load and prepare datasets."""
        from datasets import load_dataset
        
        dataset = load_dataset('json', data_files={
            'train': train_file,
            'validation': val_file,
            'test': test_file
        })
        
        # Tokenize
        def tokenize_function(examples):
            return self.tokenizer(
                examples['text'],
                padding='max_length',
                truncation=True,
                max_length=512
            )
        
        tokenized_dataset = dataset.map(tokenize_function, batched=True)
        return tokenized_dataset
    
    def setup_model(self, num_labels=2):
        """Initialize model and tokenizer."""
        from transformers import AutoModelForSequenceClassification, AutoTokenizer
        
        self.tokenizer = AutoTokenizer.from_pretrained(self.model_name)
        self.model = AutoModelForSequenceClassification.from_pretrained(
            self.model_name,
            num_labels=num_labels
        )
    
    def train(self, train_dataset, eval_dataset, output_dir='./output'):
        """Train with best practices."""
        
        # Custom callback for logging
        class WandbCallback(TrainerCallback):
            def on_log(self, args, state, control, logs=None, **kwargs):
                if logs:
                    wandb.log(logs)
        
        training_args = TrainingArguments(
            output_dir=output_dir,
            num_train_epochs=3,
            per_device_train_batch_size=16,
            per_device_eval_batch_size=64,
            warmup_ratio=0.1,
            weight_decay=0.01,
            logging_dir=f'{output_dir}/logs',
            logging_steps=100,
            evaluation_strategy="steps",
            eval_steps=500,
            save_strategy="steps",
            save_steps=500,
            load_best_model_at_end=True,
            metric_for_best_model="eval_loss",
            greater_is_better=False,
            fp16=True,  # Mixed precision training
            gradient_accumulation_steps=2,
            learning_rate=2e-5,
            lr_scheduler_type="cosine",
        )
        
        trainer = Trainer(
            model=self.model,
            args=training_args,
            train_dataset=train_dataset,
            eval_dataset=eval_dataset,
            callbacks=[WandbCallback()],
        )
        
        # Train
        trainer.train()
        
        # Save
        trainer.save_model(f'{output_dir}/final_model')
        self.tokenizer.save_pretrained(f'{output_dir}/final_model')
        
        return trainer
    
    def evaluate_and_export(self, test_dataset, output_dir='./output'):
        """Evaluate and export model."""
        evaluator = ModelEvaluator(self.model, self.tokenizer)
        
        # Evaluate
        results = evaluator.evaluate_classification(
            test_dataset['text'],
            test_dataset['label']
        )
        
        # Log to wandb
        wandb.log(results)
        
        # Save results
        with open(f'{output_dir}/evaluation_results.json', 'w') as f:
            json.dump(results, f, indent=2)
        
        # Export for deployment
        self.model.save_pretrained(f'{output_dir}/deployment')
        self.tokenizer.save_pretrained(f'{output_dir}/deployment')
        
        print(f"Model ready for deployment at {output_dir}/deployment")
        
        return results

# Usage
finetuner = ProductionFineTuner('bert-base-uncased', 'classification')
finetuner.setup_model(num_labels=2)

# Prepare data
dataset = finetuner.prepare_data(
    'train.json',
    'val.json',
    'test.json'
)

# Train
trainer = finetuner.train(
    dataset['train'],
    dataset['validation'],
    output_dir='./production_model'
)

# Evaluate and export
results = finetuner.evaluate_and_export(
    dataset['test'],
    output_dir='./production_model'
)

print(f"Final accuracy: {results['accuracy']:.4f}")

📝 Assignments & Practice Problems

Assignment 1: Fine-tune for Sentiment Analysis ⭐⭐

Difficulty: Medium | Time: 60 minutes

Task: Fine-tune BERT on a sentiment analysis dataset and achieve >85% accuracy.

Requirements:

  • Use IMDb or Twitter sentiment dataset
  • Implement proper train/val/test split
  • Track metrics with wandb
  • Save best model
  • Create inference function

Expected Output:

  • Training accuracy: >90%
  • Validation accuracy: >85%
  • Test accuracy: >85%
  • Model size: ~400MB

Assignment 2: Implement LoRA from Scratch ⭐⭐⭐

Difficulty: Hard | Time: 90 minutes

Task: Implement the LoRA algorithm without using the PEFT library.

Requirements:

class LoRALayer:
    def __init__(self, original_layer, r=8, alpha=32):
        """
        Implement LoRA for a linear layer.
        
        LoRA decomposes weight updates as:
        W' = W + BA
        where B is (d x r) and A is (r x k)
        """
        # TODO: Implement
        pass

Assignment 3: Multi-task Fine-tuning ⭐⭐⭐

Difficulty: Hard | Time: 120 minutes

Task: Fine-tune a single model on multiple tasks simultaneously.

Requirements:

  • Handle 3+ different tasks
  • Implement task-specific heads
  • Balance training across tasks
  • Evaluate on each task separately

Assignment 4: Cost-Effective Fine-tuning ⭐⭐

Difficulty: Medium | Time: 60 minutes

Task: Fine-tune a model with minimal compute budget.

Requirements:

  • Use gradient accumulation
  • Implement mixed precision training
  • Use LoRA or QLoRA
  • Optimize batch size
  • Track GPU memory usage

Assignment 5: Production Deployment Pipeline ⭐⭐⭐

Difficulty: Hard | Time: 120 minutes

Task: Create end-to-end pipeline from training to deployment.

Requirements:

  • Data preprocessing
  • Training with checkpoints
  • Evaluation and metrics
  • Model optimization (quantization)
  • API endpoint creation
  • Docker containerization

❌ High learning rate destroys pre-trained knowledge

optimizer = Adam(model.parameters(), lr=1e-3) # Too high!

✅ Use lower learning rate for fine-tuning

optimizer = Adam(model.parameters(), lr=2e-5) # Better


### Pitfall 2: Overfitting on Small Datasets

```python
# ❌ Training too long on small dataset
num_epochs = 10  # Will overfit!

# ✅ Use early stopping and regularization
training_args = TrainingArguments(
    num_train_epochs=3,
    weight_decay=0.01,
    load_best_model_at_end=True,
)

Pitfall 3: Not Freezing Layers

# ✅ Freeze early layers, fine-tune later layers
for param in model.bert.embeddings.parameters():
    param.requires_grad = False

for param in model.bert.encoder.layer[:8].parameters():
    param.requires_grad = False

🔗 Related Concepts

📚 Resources

Essential Papers

Video Tutorials

Code Libraries

pip install transformers
pip install peft  # For LoRA
pip install bitsandbytes  # For QLoRA
pip install wandb  # For tracking

🐙
Go Deeper · Code it yourself
ai-engineering-from-scratch
Hands-on fine-tuning from scratch — LoRA and QLoRA implemented line by line, covering adapter math, rank decomposition, and memory-efficient training for production use.
🔨 Phase 11.08 · Fine-Tuning with LoRA & QLoRA 🚀 Capstone 07 · End-to-End Fine-Tuning Pipeline