Fine-tuning de modelos de lenguaje con datos específicos del dominio
Ajusta modelos de lenguaje a tu dominio con LoRA y QLoRA: preparación de datasets, estrategias de entrenamiento y evaluación sin grandes presupuestos.

Los modelos de lenguaje de propósito general saben un poco de todo. Pueden escribir poesía, explicar física cuántica y generar código. Pero pregúntales sobre las APIs internas de tu empresa, la terminología específica de tu industria o las convenciones de código de tu organización, y alucinan con confianza. El ajuste fino transforma a un generalista en un especialista: un modelo que entiende el lenguaje, los patrones y las restricciones de tu dominio.
La barrera para hacer ajuste fino se ha derrumbado. Métodos eficientes en parámetros como LoRA permiten ajustar un modelo de 7B parámetros en una sola GPU en horas. No necesitas un equipo de ingenieros de ML ni un cluster de A100s. Necesitas buenos datos, objetivos claros y la disciplina de evaluar si tu modelo ajustado realmente mejora al modelo base para tu caso de uso específico.
Cuándo tiene sentido el ajuste fino
El ajuste fino no siempre es la respuesta. Antes de invertir en entrenamiento, entiende cuándo es la herramienta adecuada.
## Decision framework
### Use fine-tuning when:
- You need the model to adopt a specific style or format
(medical reports, legal briefs, code in your framework)
- Domain-specific terminology that the base model gets wrong
- You have 100+ high-quality examples of desired behavior
- Prompt engineering alone can't achieve consistent quality
- Latency matters and you can't afford long prompts with
many examples
### Use RAG (retrieval) instead when:
- The knowledge changes frequently (product catalog, docs)
- You need citations and verifiable sources
- The base model understands the domain but lacks specifics
- You have structured data that's easy to retrieve
### Use prompt engineering instead when:
- You have fewer than 50 examples
- The task is well-defined and consistent
- Few-shot examples in the prompt achieve good results
- You need flexibility to change behavior without retrainingPreparación de los datos de entrenamiento
La calidad de tu dataset de ajuste fino determina la calidad de tu modelo. Si entra basura, sale basura: aquí aplica más que en cualquier otra parte del software.
# Dataset preparation for instruction fine-tuning
import json
from typing import TypedDict
class TrainingExample(TypedDict):
instruction: str
input: str
output: str
# ❌ Low-quality training data
bad_examples = [
{
"instruction": "Write code",
"input": "make a function",
"output": "def foo(): pass" # Too vague, teaches nothing
},
{
"instruction": "Fix the bug",
"input": "it doesn't work",
"output": "try restarting" # Not actionable
},
]
# ✅ High-quality training data
good_examples: list[TrainingExample] = [
{
"instruction": (
"Write a TypeScript function that validates "
"an email address using our company's domain rules."
),
"input": (
"Requirements: Must end with @company.com or "
"@subsidiary.com. Must not contain plus addressing. "
"Must be lowercase."
),
"output": (
"function validateCompanyEmail(email: string): "
"{ valid: boolean; error?: string } {\n"
" const normalized = email.toLowerCase().trim();\n"
" \n"
" if (normalized !== email) {\n"
" return { valid: false, error: "
"'Email must be lowercase' };\n"
" }\n"
" \n"
" if (normalized.includes('+')) {\n"
" return { valid: false, error: "
"'Plus addressing not allowed' };\n"
" }\n"
" \n"
" const allowedDomains = "
"['@company.com', '@subsidiary.com'];\n"
" const hasValidDomain = allowedDomains.some(\n"
" domain => normalized.endsWith(domain)\n"
" );\n"
" \n"
" if (!hasValidDomain) {\n"
" return { valid: false, error: "
"'Must use company email domain' };\n"
" }\n"
" \n"
" return { valid: true };\n"
"}"
),
},
]
def prepare_dataset(
examples: list[TrainingExample],
output_path: str
) -> None:
"""Format examples for fine-tuning frameworks."""
formatted = []
for ex in examples:
formatted.append({
"messages": [
{
"role": "system",
"content": (
"You are a senior engineer at Company. "
"Follow our coding standards and conventions."
),
},
{
"role": "user",
"content": f"{ex['instruction']}\n\n{ex['input']}",
},
{
"role": "assistant",
"content": ex["output"],
},
]
})
with open(output_path, "w") as f:
for item in formatted:
f.write(json.dumps(item) + "\n")LoRA: ajuste fino sin la factura de GPU
Low-Rank Adaptation (LoRA) congela los pesos originales del modelo y entrena pequeñas matrices de adaptación. Esto reduce los parámetros entrenables en más del 99% mientras logra una calidad comparable al ajuste fino completo.
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import LoraConfig, get_peft_model, TaskType
from datasets import load_dataset
from trl import SFTTrainer, SFTConfig
# Load base model
model_name = "meta-llama/Llama-3.1-8B-Instruct"
model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype="auto",
device_map="auto",
)
tokenizer = AutoTokenizer.from_pretrained(model_name)
# LoRA configuration
# r: rank of the adaptation matrices (lower = fewer params)
# alpha: scaling factor (typically 2x rank)
# target_modules: which layers to adapt
# ❌ Too aggressive: high rank, all modules
bad_config = LoraConfig(
r=128, # Way too many parameters
lora_alpha=256,
target_modules="all-linear", # Unnecessary
)
# ✅ Balanced: enough capacity for domain adaptation
lora_config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
r=16,
lora_alpha=32,
lora_dropout=0.05,
target_modules=[
"q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj",
],
bias="none",
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# trainable params: 13,631,488 || all params: 8,043,167,744
# trainable%: 0.1695Bucle de entrenamiento e hiperparámetros
La configuración de entrenamiento importa más de lo que la mayoría cree. La tasa de aprendizaje, el tamaño del batch y la cantidad de épocas pueden marcar la diferencia entre un modelo útil y uno catastróficamente sobreajustado.
# Training configuration
training_config = SFTConfig(
output_dir="./checkpoints",
# Learning rate: too high = catastrophic forgetting
# too low = you're just wasting compute
learning_rate=2e-4,
# Warmup prevents early instability
warmup_steps=100,
lr_scheduler_type="cosine",
# Epochs: 1-3 for most datasets
# More epochs = more overfitting risk
num_train_epochs=2,
per_device_train_batch_size=4,
gradient_accumulation_steps=4, # Effective batch size: 16
# Evaluation during training to catch overfitting
eval_strategy="steps",
eval_steps=50,
save_strategy="steps",
save_steps=50,
# Mixed precision for memory efficiency
bf16=True,
logging_steps=10,
max_seq_length=2048,
)
# Load dataset
dataset = load_dataset("json", data_files={
"train": "data/train.jsonl",
"eval": "data/eval.jsonl",
})
# Initialize trainer
trainer = SFTTrainer(
model=model,
args=training_config,
train_dataset=dataset["train"],
eval_dataset=dataset["eval"],
processing_class=tokenizer,
)
# Train
trainer.train()
# Save the adapter (not the full model)
trainer.save_model("./final-adapter")
# Adapter is ~50MB vs 16GB for the full modelEvaluación: ¿el modelo ajustado realmente ayuda?
El paso más importante es la evaluación rigurosa. Un modelo que obtiene buena loss de entrenamiento pero produce peores salidas que el modelo base con buen prompting es una pérdida de tiempo.
# Evaluation framework
from dataclasses import dataclass
@dataclass
class EvalResult:
example_id: str
base_output: str
finetuned_output: str
reference_output: str
base_score: float
finetuned_score: float
def evaluate_model(
base_model,
finetuned_model,
eval_examples: list[dict],
scorer,
) -> list[EvalResult]:
"""Compare base vs fine-tuned on held-out examples."""
results = []
for example in eval_examples:
prompt = example["instruction"] + "\n" + example["input"]
base_output = generate(base_model, prompt)
ft_output = generate(finetuned_model, prompt)
base_score = scorer.score(
base_output, example["output"]
)
ft_score = scorer.score(
ft_output, example["output"]
)
results.append(EvalResult(
example_id=example["id"],
base_output=base_output,
finetuned_output=ft_output,
reference_output=example["output"],
base_score=base_score,
finetuned_score=ft_score,
))
# Summary statistics
base_avg = sum(r.base_score for r in results) / len(results)
ft_avg = sum(r.finetuned_score for r in results) / len(results)
print(f"Base model average: {base_avg:.3f}")
print(f"Fine-tuned average: {ft_avg:.3f}")
print(f"Improvement: {(ft_avg - base_avg) / base_avg * 100:.1f}%")
# Check for regressions: cases where fine-tuned is worse
regressions = [
r for r in results
if r.finetuned_score < r.base_score
]
print(f"Regressions: {len(regressions)}/{len(results)}")
return resultsErrores comunes
# ❌ Pitfall 1: Overfitting on small datasets
# Symptom: training loss drops to near zero,
# eval loss increases
# Fix: fewer epochs, more dropout, more data
# ❌ Pitfall 2: Catastrophic forgetting
# Symptom: domain task improves but general
# capability degrades
# Fix: lower learning rate, mix in general data (5-10%)
# ❌ Pitfall 3: Poor data quality
# Symptom: model learns your mistakes and inconsistencies
# Fix: have domain experts review training examples
# Quality check: would you want a junior dev to
# learn from this example?
# ✅ Practical data quality checklist:
# - Every example has a clear, complete answer
# - No contradictions between examples
# - Consistent formatting and style
# - At least 100 examples for narrow tasks
# - At least 500 for broader domain adaptation
# - 10-20% held out for evaluationConclusiones clave
El ajuste fino es la elección correcta cuando necesitas un estilo, formato o terminología de dominio consistentes que el prompting solo no logra de forma confiable, pero si tu conocimiento cambia frecuentemente o necesitas citas, la generación aumentada con recuperación (RAG) suele ser una mejor opción. LoRA hace el ajuste fino accesible en hardware de consumo al entrenar pequeñas matrices adaptadoras (0,17% de los parámetros totales) mientras logra resultados comparables al ajuste fino completo: empieza con rank 16, apunta a las capas de proyección de atención y feed-forward, y usa un schedule de tasa de aprendizaje coseno comenzando en 2e-4. La calidad de los datos importa más que la cantidad: 200 ejemplos curados por expertos, donde cada uno demuestra exactamente el comportamiento que buscas, superan a 10.000 ejemplos ruidosos extraídos de la web, y cada ejemplo de entrenamiento debería pasar la prueba de "¿querría que un desarrollador junior aprendiera de esto?". Evalúa siempre contra el modelo base en ejemplos reservados antes de desplegar, porque un modelo ajustado que tiene buena loss de entrenamiento pero produce peores salidas que el modelo base con buen prompting tiene valor negativo: revisa regresiones y olvido catastrófico, no solo la mejora promedio.


