Fine-tuning Aplicado
El fine-tuning permite adaptar un LLM existente a tu dominio o estilo. En esta lección aprenderás cuándo usarlo (vs RAG), cómo preparar datasets, ejecutar fine-tuning via API y evaluar los resultados.
¿Cuándo hacer fine-tuning?
| Situación | Solución |
|---|---|
| Datos que cambian frecuentemente | RAG |
| Necesitas citar fuentes específicas | RAG |
| Cambiar el tono/estilo de respuesta | Fine-tuning |
| Dominio muy especializado (médico, legal) | Fine-tuning |
| Formato de output específico y consistente | Fine-tuning |
| Reducir latencia (eliminar retrieval) | Fine-tuning |
| Reducir costos (modelo más pequeño) | Fine-tuning |
| Ambas: conocimiento + estilo | Fine-tuning + RAG |
Complejidad: Prompt Engineering → RAG → Fine-tuning → Entrenar desde cero
Costo: $0 $$ $$$ $$$$$$
Tiempo: Minutos Horas Días Meses
Preparación del Dataset
Formato JSONL
{"messages": [{"role": "system", "content": "Eres un experto en derecho laboral mexicano."}, {"role": "user", "content": "¿Cuántos días de vacaciones corresponden después de un año?"}, {"role": "assistant", "content": "Según el artículo 76 de la Ley Federal del Trabajo (reformado en 2023), después del primer año de trabajo corresponden **12 días** de vacaciones. Este número incrementa 2 días por año hasta llegar a 20 días, y después aumenta 2 días por cada 5 años adicionales."}]}
{"messages": [{"role": "system", "content": "Eres un experto en derecho laboral mexicano."}, {"role": "user", "content": "¿Qué es el aguinaldo y cuánto es?"}, {"role": "assistant", "content": "El aguinaldo es una prestación obligatoria establecida en el artículo 87 de la Ley Federal del Trabajo. Corresponde a un mínimo de **15 días de salario**, pagado antes del 20 de diciembre. Trabajadores con menos de un año reciben la parte proporcional."}]}
Script para generar dataset
import { writeFileSync } from 'fs';
interface TrainingExample {
messages: {
role: 'system' | 'user' | 'assistant';
content: string;
}[];
}
function generateTrainingData(
systemPrompt: string,
examples: { question: string; answer: string }[]
): TrainingExample[] {
return examples.map(({ question, answer }) => ({
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: question },
{ role: 'assistant', content: answer },
],
}));
}
// Generar dataset
const dataset = generateTrainingData(
'Eres un asistente de soporte técnico para la app SuperChat.',
[
{
question: '¿Cómo cambio mi contraseña?',
answer: 'Para cambiar tu contraseña:\n1. Ve a Configuración > Seguridad\n2. Haz clic en "Cambiar contraseña"\n3. Ingresa tu contraseña actual y la nueva\n4. Confirma con "Guardar"\n\nLa contraseña debe tener mínimo 8 caracteres, una mayúscula y un número.',
},
// ... más ejemplos (mínimo 10, recomendado 50-100+)
]
);
// Guardar como JSONL
const jsonl = dataset.map(d => JSON.stringify(d)).join('\n');
writeFileSync('training_data.jsonl', jsonl);
Validación del dataset
function validateDataset(filePath: string): {
valid: boolean;
errors: string[];
stats: Record<string, number>;
} {
const lines = readFileSync(filePath, 'utf-8').trim().split('\n');
const errors: string[] = [];
let totalTokens = 0;
for (let i = 0; i < lines.length; i++) {
try {
const example = JSON.parse(lines[i]);
if (!example.messages || !Array.isArray(example.messages)) {
errors.push(`Line ${i + 1}: missing 'messages' array`);
continue;
}
const roles = example.messages.map((m: any) => m.role);
if (!roles.includes('user') || !roles.includes('assistant')) {
errors.push(`Line ${i + 1}: must have 'user' and 'assistant' messages`);
}
// Estimar tokens (aprox 4 chars = 1 token)
const chars = example.messages.reduce(
(sum: number, m: any) => sum + m.content.length, 0
);
totalTokens += Math.ceil(chars / 4);
} catch {
errors.push(`Line ${i + 1}: invalid JSON`);
}
}
return {
valid: errors.length === 0,
errors,
stats: {
totalExamples: lines.length,
estimatedTokens: totalTokens,
estimatedCost: totalTokens * 0.000008, // Precio aproximado GPT-4o-mini fine-tuning
},
};
}
Fine-tuning con OpenAI API
Paso 1: Subir el archivo
const file = await openai.files.create({
file: createReadStream('training_data.jsonl'),
purpose: 'fine-tune',
});
console.log(`File uploaded: ${file.id}`);
Paso 2: Crear el job de fine-tuning
const job = await openai.fineTuning.jobs.create({
training_file: file.id,
model: 'gpt-4o-mini-2024-07-18', // Modelo base
hyperparameters: {
n_epochs: 3, // Número de epochs (auto para automático)
batch_size: 'auto',
learning_rate_multiplier: 'auto',
},
suffix: 'mi-asistente-soporte', // Identificador custom
});
console.log(`Job created: ${job.id}`);
console.log(`Status: ${job.status}`);
Paso 3: Monitorear el progreso
async function monitorFineTuning(jobId: string) {
while (true) {
const job = await openai.fineTuning.jobs.retrieve(jobId);
console.log(`Status: ${job.status}`);
if (job.status === 'succeeded') {
console.log(`✅ Model ready: ${job.fine_tuned_model}`);
return job.fine_tuned_model;
}
if (job.status === 'failed') {
console.error(`❌ Fine-tuning failed:`, job.error);
throw new Error('Fine-tuning failed');
}
// Ver eventos
const events = await openai.fineTuning.jobs.listEvents(jobId, { limit: 5 });
for (const event of events.data) {
console.log(` [${event.created_at}] ${event.message}`);
}
await new Promise(r => setTimeout(r, 30_000)); // Esperar 30s
}
}
Paso 4: Usar el modelo fine-tuned
const response = await openai.chat.completions.create({
model: 'ft:gpt-4o-mini-2024-07-18:my-org:mi-asistente-soporte:abc123',
messages: [
{ role: 'system', content: 'Eres un asistente de soporte técnico para SuperChat.' },
{ role: 'user', content: '¿Cómo exporto mis datos?' },
],
});
LoRA: Fine-tuning eficiente para modelos open source
# Ejemplo con Hugging Face + PEFT (Python)
from peft import LoraConfig, get_peft_model
from transformers import AutoModelForCausalLM, AutoTokenizer
# Cargar modelo base
model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-3.1-8B")
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-3.1-8B")
# Configurar LoRA
lora_config = LoraConfig(
r=16, # Rango de las matrices
lora_alpha=32, # Factor de escala
target_modules=["q_proj", "v_proj"], # Capas a adaptar
lora_dropout=0.05,
task_type="CAUSAL_LM",
)
# Crear modelo con LoRA
peft_model = get_peft_model(model, lora_config)
# Solo ~0.1% de parámetros entrenables!
peft_model.print_trainable_parameters()
# trainable params: 4,194,304 || all params: 8,030,261,248 || trainable%: 0.0522
QLoRA: LoRA + Cuantización
from transformers import BitsAndBytesConfig
import torch
# Cuantización a 4-bit
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16,
)
# Cargar modelo cuantizado
model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-3.1-8B",
quantization_config=bnb_config,
device_map="auto",
)
# Ahora LoRA sobre el modelo cuantizado
# Reduce de 80GB a ~6GB de VRAM!
Evaluación del modelo fine-tuned
interface EvalResult {
question: string;
expected: string;
actual: string;
score: number;
}
async function evaluateModel(
modelId: string,
testSet: { question: string; expectedAnswer: string }[]
): Promise<EvalResult[]> {
const results: EvalResult[] = [];
for (const { question, expectedAnswer } of testSet) {
const response = await openai.chat.completions.create({
model: modelId,
messages: [{ role: 'user', content: question }],
});
const actual = response.choices[0].message.content!;
// Evaluar con LLM-as-Judge
const judge = await openai.chat.completions.create({
model: 'gpt-4o',
response_format: { type: 'json_object' },
messages: [{
role: 'user',
content: `Evalúa si la respuesta es correcta y completa.
Pregunta: ${question}
Respuesta esperada: ${expectedAnswer}
Respuesta generada: ${actual}
Retorna JSON: { "score": 1-10, "feedback": "..." }`,
}],
});
const evaluation = JSON.parse(judge.choices[0].message.content!);
results.push({
question,
expected: expectedAnswer,
actual,
score: evaluation.score,
});
}
const avgScore = results.reduce((s, r) => s + r.score, 0) / results.length;
console.log(`Average score: ${avgScore.toFixed(2)}/10`);
return results;
}
Costos de fine-tuning
| Modelo | Training (1M tokens) | Input (1M) | Output (1M) |
|---|---|---|---|
gpt-4o-mini fine-tuned |
$3.00 | $0.30 | $1.20 |
gpt-4o fine-tuned |
$25.00 | $3.75 | $15.00 |
| Llama 3.1 8B (propio) | Costo GPU | $0 | $0 |
Un dataset de 100 ejemplos ≈ 50K tokens ≈ $0.15 de training con GPT-4o-mini.