GEMINI LABJP
NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaplyOMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflowsAGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactionsMEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuouslyTHROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and regionDEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaplyOMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflowsAGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactionsMEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuouslyTHROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and regionDEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20
Articles/API / SDK
API / SDK/2026-03-14Advanced

Fine-Tuning Gemini: Building Specialized Models for Domain-Specific Applications

Complete guide to fine-tuning Gemini models. Learn dataset preparation, training strategies, evaluation metrics, and production deployment for custom AI.

fine-tuning2model-customizationtraininggemini-api277llm-optimization

Fine-Tuning Gemini: Building Specialized Models for Domain-Specific Applications

Off-the-shelf language models excel at general tasks. But when you need a model that understands your domain's language patterns, industry jargon, or specialized reasoning, fine-tuning becomes essential. Gemini's fine-tuning API enables you to build models that perform like experts in your field.

Understanding Fine-Tuning: When and Why

Fine-tuning adjusts a pre-trained model's weights using your data. This is different from prompt engineering or RAG, which work with the model as-is. Fine-tuning permanently changes the model's behavior.

💡
Fine-tuning is most effective when: - You have domain-specific vocabulary (legal documents, medical notes, financial reports) - You need consistent output formatting (structured data extraction, specific JSON schemas) - Your domain has unique reasoning patterns (specialized diagnostic procedures) - You're willing to invest in quality data preparation (at least 100-200 high-quality examples)

It's less effective for tasks better solved by RAG, few-shot prompting, or basic tool integration.

Fine-Tuning vs. Alternatives

# Scenario: Extract structured insights from compliance documents
 
# Option 1: Few-shot prompting (quick, limited)
def extract_with_fewshot(document: str) -> dict:
    response = client.messages.create(
        model="claude-3-5-sonnet-20241022",
        max_tokens=1000,
        messages=[{
            "role": "user",
            "content": f"""
            Extract compliance violations from this document.
 
            Example 1:
            Input: "System was down for 4 hours on 2024-03-14"
            Output: {{"violation": "SLA breach", "duration_hours": 4, "date": "2024-03-14"}}
 
            Now extract from: {document}
            """
        }]
    )
    return json.loads(response.content[0].text)
 
# Option 2: RAG with domain docs (retrieval-focused)
def extract_with_rag(document: str) -> dict:
    # Retrieve similar examples from knowledge base
    examples = retrieve_examples(document, top_k=3)
    # Use retrieved examples in prompt
    pass
 
# Option 3: Fine-tuned model (specialized)
def extract_with_finetuned(document: str) -> dict:
    response = client.messages.create(
        model="compliance-extractor-v1",  # Fine-tuned model
        max_tokens=1000,
        messages=[{
            "role": "user",
            "content": f"Extract violations: {document}"
        }]
    )
    return json.loads(response.content[0].text)

For compliance extraction where you process thousands of documents and need consistent, specialized extraction, fine-tuning pays for itself.

Dataset Preparation: The Foundation

Fine-tuning quality depends entirely on your data. Garbage in, garbage out applies here more than anywhere else.

Data Format and Structure

Gemini expects training data in a specific format. Each training example should include:

from dataclasses import dataclass
from typing import List, Optional
import json
 
@dataclass
class TrainingExample:
    """A single training example for fine-tuning."""
    input_text: str
    output_text: str
    metadata: Optional[dict] = None
 
class DatasetPreparer:
    def __init__(self, output_format: str = "jsonl"):
        self.output_format = output_format
        self.examples: List[TrainingExample] = []
 
    def add_example(
        self,
        input_text: str,
        output_text: str,
        metadata: dict = None
    ):
        """Add a training example."""
        example = TrainingExample(
            input_text=input_text,
            output_text=output_text,
            metadata=metadata or {}
        )
        self.examples.append(example)
 
    def validate_examples(self) -> dict:
        """Validate dataset quality before training."""
        validation_report = {
            "total_examples": len(self.examples),
            "avg_input_length": 0,
            "avg_output_length": 0,
            "length_distribution": {},
            "potential_issues": [],
        }
 
        if len(self.examples) < 100:
            validation_report["potential_issues"].append(
                "Dataset smaller than recommended minimum (100 examples)"
            )
 
        input_lengths = []
        output_lengths = []
 
        for example in self.examples:
            input_len = len(example.input_text.split())
            output_len = len(example.output_text.split())
            input_lengths.append(input_len)
            output_lengths.append(output_len)
 
        validation_report["avg_input_length"] = sum(input_lengths) / len(input_lengths)
        validation_report["avg_output_length"] = sum(output_lengths) / len(output_lengths)
 
        # Check for duplicates
        unique_inputs = len(set(e.input_text for e in self.examples))
        if unique_inputs < len(self.examples) * 0.9:
            validation_report["potential_issues"].append(
                f"High duplication rate: {1 - unique_inputs/len(self.examples):.1%}"
            )
 
        # Check for output consistency
        if max(output_lengths) > 4000:
            validation_report["potential_issues"].append(
                "Some outputs exceed recommended max length (4000 tokens)"
            )
 
        return validation_report
 
    def export_training_data(
        self,
        filepath: str,
        include_metadata: bool = True,
        train_split: float = 0.8
    ):
        """
        Export training data in JSONL format for Gemini API.
        Includes automatic train/eval split.
        """
 
        import random
        random.shuffle(self.examples)
 
        split_idx = int(len(self.examples) * train_split)
        train_examples = self.examples[:split_idx]
        eval_examples = self.examples[split_idx:]
 
        # Export training set
        with open(f"{filepath}.train.jsonl", "w") as f:
            for example in train_examples:
                record = {
                    "messages": [
                        {
                            "role": "user",
                            "content": example.input_text,
                        },
                        {
                            "role": "assistant",
                            "content": example.output_text,
                        }
                    ]
                }
                if include_metadata and example.metadata:
                    record["metadata"] = example.metadata
                f.write(json.dumps(record) + "\n")
 
        # Export eval set
        with open(f"{filepath}.eval.jsonl", "w") as f:
            for example in eval_examples:
                record = {
                    "messages": [
                        {
                            "role": "user",
                            "content": example.input_text,
                        },
                        {
                            "role": "assistant",
                            "content": example.output_text,
                        }
                    ]
                }
                if include_metadata and example.metadata:
                    record["metadata"] = example.metadata
                f.write(json.dumps(record) + "\n")
 
        return {
            "train_examples": len(train_examples),
            "eval_examples": len(eval_examples),
            "train_file": f"{filepath}.train.jsonl",
            "eval_file": f"{filepath}.eval.jsonl",
        }
 
# Real-world example: Legal document analysis
preparer = DatasetPreparer()
 
# Example 1: Contract analysis
preparer.add_example(
    input_text="This Agreement is made effective as of January 1, 2024. "
               "The Licensor grants a non-exclusive, worldwide license to use the Software "
               "for internal business purposes only. All warranty is disclaimed.",
    output_text=json.dumps({
        "contract_type": "Software License",
        "effective_date": "2024-01-01",
        "scope": "non-exclusive, worldwide",
        "usage": "internal business purposes",
        "warranty": "disclaimed",
        "key_clauses": ["license grant", "warranty disclaimer"]
    }),
    metadata={"document_id": "contract_001", "category": "license"}
)
 
# Example 2: Risk identification
preparer.add_example(
    input_text="Liability shall be limited to direct damages not exceeding the total fees "
               "paid in the prior 12 months. Neither party shall be liable for indirect, "
               "incidental, or consequential damages.",
    output_text=json.dumps({
        "liability_cap": "direct damages only",
        "cap_amount": "total fees (12 months)",
        "excluded_damages": ["indirect", "incidental", "consequential"],
        "risk_level": "low"
    }),
    metadata={"document_id": "contract_001", "category": "liability"}
)
 
# Validate and export
report = preparer.validate_examples()
print("Validation Report:", report)
 
export_info = preparer.export_training_data(
    "legal_analysis_dataset",
    train_split=0.8
)
print("Export Info:", export_info)
⚠️
Data quality directly impacts model quality. Spend time: - Removing duplicates - Fixing inconsistent formatting - Validating output correctness - Ensuring examples are representative - Checking for biases or artifacts

A smaller, cleaner dataset often beats a larger, noisier one.

Training Configuration and Hyperparameters

Once your data is ready, configure the training job. Gemini exposes key hyperparameters:

import anthropic
import json
from typing import Optional
 
class GeminiFineTuner:
    def __init__(self, api_key: str = None):
        self.client = anthropic.Anthropic(api_key=api_key)
 
    def create_fine_tuning_job(
        self,
        training_file_path: str,
        eval_file_path: str,
        model_id: str = "compliance-extractor-v1",
        display_name: str = "Compliance Document Extractor",
        learning_rate: float = 0.001,
        num_epochs: int = 3,
        batch_size: int = 32,
        weight_decay: float = 0.01,
        warmup_steps: int = 100,
    ) -> dict:
        """
        Create a fine-tuning job with specified hyperparameters.
 
        Args:
            training_file_path: Path to JSONL training data
            eval_file_path: Path to JSONL evaluation data
            model_id: Identifier for the fine-tuned model
            learning_rate: Learning rate for optimization
            num_epochs: Number of training epochs
            batch_size: Batch size for training
            weight_decay: L2 regularization strength
            warmup_steps: Steps for learning rate warmup
 
        Returns:
            Job metadata including job_id and status
        """
 
        # Upload training data
        with open(training_file_path, "rb") as f:
            train_response = self.client.beta.files.upload(
                file=f,
            )
        train_file_id = train_response.id
 
        # Upload eval data
        with open(eval_file_path, "rb") as f:
            eval_response = self.client.beta.files.upload(
                file=f,
            )
        eval_file_id = eval_response.id
 
        # Create fine-tuning job
        job_response = self.client.beta.messages.create(
            model="claude-3-5-sonnet-20241022",
            max_tokens=1024,
            messages=[
                {
                    "role": "user",
                    "content": (
                        f"Create a fine-tuning job with the following parameters:\n"
                        f"Model ID: {model_id}\n"
                        f"Display Name: {display_name}\n"
                        f"Training File: {train_file_id}\n"
                        f"Eval File: {eval_file_id}\n"
                        f"Learning Rate: {learning_rate}\n"
                        f"Epochs: {num_epochs}\n"
                        f"Batch Size: {batch_size}\n"
                        f"Weight Decay: {weight_decay}\n"
                        f"Warmup Steps: {warmup_steps}"
                    ),
                }
            ],
        )
 
        return {
            "job_id": model_id,
            "display_name": display_name,
            "training_file_id": train_file_id,
            "eval_file_id": eval_file_id,
            "hyperparameters": {
                "learning_rate": learning_rate,
                "num_epochs": num_epochs,
                "batch_size": batch_size,
                "weight_decay": weight_decay,
                "warmup_steps": warmup_steps,
            },
            "status": "submitted",
        }
 
    def monitor_training(self, job_id: str) -> dict:
        """
        Monitor training job progress.
        Returns metrics and current status.
        """
 
        # Implementation would poll the training API
        # and return progress metrics
        return {
            "job_id": job_id,
            "status": "training",
            "epoch": 2,
            "total_epochs": 3,
            "loss": 0.245,
            "eval_loss": 0.312,
            "estimated_time_remaining": 3600,
        }

Hyperparameter Tuning Strategy

class HyperparameterOptimizer:
    def __init__(self, base_model: str = "gemini-pro"):
        self.base_model = base_model
        self.trials = []
 
    def grid_search(
        self,
        training_file: str,
        eval_file: str,
        param_grid: dict,
    ) -> list:
        """
        Run multiple training jobs with different hyperparameters.
        Useful for finding optimal settings for your domain.
        """
 
        trials = []
 
        for learning_rate in param_grid.get("learning_rate", [0.001]):
            for batch_size in param_grid.get("batch_size", [32]):
                for num_epochs in param_grid.get("num_epochs", [3]):
                    trial = {
                        "learning_rate": learning_rate,
                        "batch_size": batch_size,
                        "num_epochs": num_epochs,
                        "status": "pending",
                    }
                    trials.append(trial)
 
        # Submit all trials
        for trial in trials:
            # Submit training job
            pass
 
        return trials
 
    def get_best_hyperparameters(
        self,
        metric: str = "eval_f1"
    ) -> dict:
        """
        Analyze completed trials and return best hyperparameters.
        """
 
        completed = [t for t in self.trials if t["status"] == "completed"]
 
        if not completed:
            return None
 
        best = max(completed, key=lambda t: t.get(metric, 0))
        return best

Evaluation: Measuring Model Quality

Before deploying, rigorously evaluate your fine-tuned model. Generic metrics (loss, accuracy) matter less than domain-specific metrics.

💡
Choose evaluation metrics that match your business goals. For legal document analysis, you might care about: - Precision of clause extraction (false positives are costly) - Recall of risk indicators (missing risks is dangerous) - Consistency across document types - Human reviewer alignment
from typing import List, Dict
import json
from dataclasses import dataclass
 
@dataclass
class EvaluationMetrics:
    precision: float
    recall: float
    f1: float
    accuracy: float
    custom_metrics: Dict[str, float] = None
 
class ModelEvaluator:
    def __init__(self, model_id: str, client):
        self.model_id = model_id
        self.client = client
        self.predictions = []
        self.ground_truth = []
 
    def evaluate_on_dataset(
        self,
        eval_file_path: str,
        extraction_task: bool = True,
    ) -> EvaluationMetrics:
        """
        Evaluate model on evaluation dataset.
        Supports structured extraction and classification tasks.
        """
 
        # Load evaluation data
        eval_examples = []
        with open(eval_file_path, "r") as f:
            for line in f:
                eval_examples.append(json.loads(line))
 
        predictions = []
        ground_truth = []
 
        # Generate predictions
        for example in eval_examples[:100]:  # Sample for cost
            messages = example.get("messages", [])
            user_message = next(
                (m for m in messages if m["role"] == "user"),
                None
            )
            expected_response = next(
                (m for m in messages if m["role"] == "assistant"),
                None
            )
 
            if not user_message or not expected_response:
                continue
 
            # Get model prediction
            response = self.client.messages.create(
                model=self.model_id,
                max_tokens=1000,
                messages=[{"role": "user", "content": user_message["content"]}],
            )
 
            prediction = response.content[0].text
            predictions.append(prediction)
            ground_truth.append(expected_response["content"])
 
        # Calculate metrics
        if extraction_task:
            metrics = self._evaluate_extraction(predictions, ground_truth)
        else:
            metrics = self._evaluate_classification(predictions, ground_truth)
 
        return metrics
 
    def _evaluate_extraction(
        self,
        predictions: List[str],
        ground_truth: List[str]
    ) -> EvaluationMetrics:
        """
        Evaluate structured extraction tasks.
        Parses JSON and measures field accuracy.
        """
 
        correct_extractions = 0
        total_fields = 0
        extracted_fields = 0
 
        for pred, truth in zip(predictions, ground_truth):
            try:
                pred_json = json.loads(pred)
                truth_json = json.loads(truth)
 
                # Count matching fields
                for key in truth_json:
                    total_fields += 1
                    if key in pred_json and pred_json[key] == truth_json[key]:
                        correct_extractions += 1
                    extracted_fields += 1
 
            except json.JSONDecodeError:
                continue
 
        precision = correct_extractions / extracted_fields if extracted_fields > 0 else 0
        recall = correct_extractions / total_fields if total_fields > 0 else 0
        f1 = 2 * (precision * recall) / (precision + recall) if (precision + recall) > 0 else 0
 
        return EvaluationMetrics(
            precision=precision,
            recall=recall,
            f1=f1,
            accuracy=correct_extractions / total_fields if total_fields > 0 else 0,
            custom_metrics={
                "correct_extractions": correct_extractions,
                "total_fields": total_fields,
                "extraction_rate": extracted_fields / len(predictions) if predictions else 0,
            }
        )
 
    def _evaluate_classification(
        self,
        predictions: List[str],
        ground_truth: List[str]
    ) -> EvaluationMetrics:
        """Evaluate classification tasks."""
 
        correct = sum(1 for p, t in zip(predictions, ground_truth) if p.strip() == t.strip())
        total = len(predictions)
        accuracy = correct / total if total > 0 else 0
 
        return EvaluationMetrics(
            precision=accuracy,
            recall=accuracy,
            f1=accuracy,
            accuracy=accuracy,
        )
 
    def error_analysis(
        self,
        predictions: List[str],
        ground_truth: List[str],
    ) -> dict:
        """
        Identify systematic errors in model predictions.
        Helps understand where fine-tuning failed.
        """
 
        error_patterns = {
            "missing_fields": [],
            "incorrect_values": [],
            "formatting_issues": [],
            "hallucinations": [],
        }
 
        for pred, truth in zip(predictions, ground_truth):
            try:
                pred_json = json.loads(pred)
                truth_json = json.loads(truth)
 
                # Check for missing fields
                missing = set(truth_json.keys()) - set(pred_json.keys())
                if missing:
                    error_patterns["missing_fields"].append({
                        "missing": list(missing),
                        "prediction": pred_json,
                    })
 
                # Check for incorrect values
                for key in truth_json:
                    if key in pred_json and pred_json[key] != truth_json[key]:
                        error_patterns["incorrect_values"].append({
                            "field": key,
                            "expected": truth_json[key],
                            "got": pred_json[key],
                        })
 
            except json.JSONDecodeError:
                error_patterns["formatting_issues"].append({
                    "prediction": pred,
                })
 
        return error_patterns

Production Deployment

Once satisfied with evaluation results, deploy the model with proper versioning and monitoring.

class FineTunedModelDeployment:
    def __init__(self, model_id: str, client):
        self.model_id = model_id
        self.client = client
        self.deployment_config = {}
 
    def prepare_for_production(
        self,
        fallback_model: str = "claude-3-5-sonnet-20241022",
        max_retries: int = 3,
        timeout_seconds: int = 30,
    ) -> dict:
        """
        Prepare model for production deployment.
        Includes fallback strategy and error handling.
        """
 
        self.deployment_config = {
            "primary_model": self.model_id,
            "fallback_model": fallback_model,
            "max_retries": max_retries,
            "timeout_seconds": timeout_seconds,
            "health_check_interval": 3600,
            "enable_monitoring": True,
            "enable_logging": True,
        }
 
        return self.deployment_config
 
    def call_with_fallback(
        self,
        user_message: str,
        **kwargs
    ) -> str:
        """
        Call the fine-tuned model with automatic fallback.
        If the primary model fails, falls back to base model.
        """
 
        for attempt in range(self.deployment_config["max_retries"]):
            try:
                response = self.client.messages.create(
                    model=self.deployment_config["primary_model"],
                    messages=[{"role": "user", "content": user_message}],
                    timeout=self.deployment_config["timeout_seconds"],
                    **kwargs
                )
                return response.content[0].text
 
            except Exception as e:
                if attempt < self.deployment_config["max_retries"] - 1:
                    continue
 
                # Fallback to base model
                print(f"Primary model failed: {e}. Falling back...")
                response = self.client.messages.create(
                    model=self.deployment_config["fallback_model"],
                    messages=[{"role": "user", "content": user_message}],
                    **kwargs
                )
                return response.content[0].text
 
    def monitor_model_performance(self) -> dict:
        """
        Monitor fine-tuned model performance in production.
        Track latency, error rates, and output quality.
        """
 
        return {
            "model_id": self.model_id,
            "requests_processed": 0,
            "avg_latency_ms": 0,
            "error_rate": 0,
            "fallback_rate": 0,
            "last_check": None,
        }

Real-World Example: Financial Analysis Model

Here's a complete example of fine-tuning a model for financial document analysis:

# Step 1: Prepare training data
fintech_preparer = DatasetPreparer()
 
financial_examples = [
    ("EBITDA increased 23% YoY to $4.2B", "positive_growth"),
    ("Operating margin compressed from 18% to 15%", "negative_trend"),
    ("Free cash flow declined due to working capital changes", "concern"),
]
 
for input_text, label in financial_examples:
    fintech_preparer.add_example(
        input_text=f"Classify the financial sentiment: {input_text}",
        output_text=json.dumps({"sentiment": label, "confidence": 0.95}),
        metadata={"type": "sentiment_classification"}
    )
 
# Step 2: Export and validate
export_info = fintech_preparer.export_training_data("fintech_dataset")
 
# Step 3: Train model
tuner = GeminiFineTuner()
job = tuner.create_fine_tuning_job(
    training_file_path="fintech_dataset.train.jsonl",
    eval_file_path="fintech_dataset.eval.jsonl",
    model_id="fintech-sentiment-v1",
    learning_rate=0.0005,
    num_epochs=2,
    batch_size=16,
)
 
# Step 4: Evaluate
evaluator = ModelEvaluator("fintech-sentiment-v1", client)
metrics = evaluator.evaluate_on_dataset("fintech_dataset.eval.jsonl")
 
# Step 5: Deploy
deployment = FineTunedModelDeployment("fintech-sentiment-v1", client)
deployment.prepare_for_production()
 
# Step 6: Use in production
result = deployment.call_with_fallback(
    "Classify: Revenue growth of 15% YoY despite market headwinds"
)

Wrapping up

Fine-tuning Gemini transforms a general-purpose model into a specialist for your domain. The key to success is investing in quality data, carefully choosing evaluation metrics that match your business goals, and thoroughly testing before production.

Start small—fine-tune on a focused task with high-quality data. Once you see consistent improvements, expand to related tasks. And always maintain a fallback strategy to the base model as you shift traffic to your custom versions.

Share

Thank You for Reading

Gemini Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

API / SDK2026-07-18
The Same gemini-flash-latest Pointed to Different Models in Different Regions
Alias resolution rolls out region by region. Send the same gemini-flash-latest to Tokyo and us-central1 and, for a few days, different models answer. Here is why that quietly invalidates your comparisons, and how a one-shot probe catches it.
API / SDK2026-07-15
Sample what you already accepted — an audit budget that catches silent quality drift
A confidence gate only ever looks at output the model hesitated on. Silent drift sinks into the batch that sailed through. Working from a fixed 30-minutes-a-day review budget, this walks through deriving detection time from the binomial, reallocating the same budget across risk strata, and catching slow decay with a cumulative monitor.
API / SDK2026-07-14
Before One Runaway Experiment Drains the Shared Budget: Using AI Studio Spend Caps as Isolation Walls
When you run several Gemini experiments under one billing account, a single runaway loop takes everything else down with it. Here is how I use AI Studio's per-project spend caps as isolation walls, plus a client-side soft ceiling and monthly reconciliation, with working code.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →