Inicio / Artículos / Anclar el fine-tuning de un LLM con una semilla de datos reales verificada a mano

Anclar el fine-tuning de un LLM con una semilla de datos reales verificada a mano

Cómo construir una pequeña semilla de datos reales para fine-tuning: elegir fuentes de autoridad, obtener páginas protegidas y PDF como Markdown, dar formato a las filas, validar y reservar un holdout.

1504 palabras

Entrena solo con datos sintéticos, ciclo tras ciclo, y el modelo se aleja un poco más cada vez del conocimiento real que debía absorber. Para el tercer o cuarto reentrenamiento imita bien cómo suena el texto del dominio y expresa mal lo que el dominio dice.

El remedio es una semilla de datos reales: un conjunto pequeño de ejemplos, revisados a mano, tomados de fuentes con autoridad antes de que empiece cualquier expansión sintética. Tiene que ser lo bastante pequeña como para que puedas verificar cada fila. Tiene que tener autoridad, porque el modelo tratará las filas como la verdad. Y a menudo tiene que venir de sitios a los que los crawlers normales no llegan, ya que buena parte del conocimiento de dominio más valioso está detrás de controles de acceso.

A continuación se explica cómo reunir esa semilla, darle forma de filas de entrenamiento, validarla y comprobar si el fine-tuning aguantó. Los temas, en orden:

  • por qué un bucle puramente sintético deriva
  • cómo elegir las fuentes adecuadas
  • cómo obtener páginas protegidas como Markdown limpio
  • qué hacer con PDF y otros documentos que no son HTML
  • cómo convertir el contenido en pares instrucción-respuesta, pares de preferencia o texto de dominio sin más
  • cómo validar la semilla y medir el resultado

Por qué los bucles solo sintéticos derivan

Estadísticamente, el fine-tuning se comporta distinto que el preentrenamiento. En el preentrenamiento el ruido se reparte entre miles de millones de tokens, así que un párrafo malo se pierde en la media. Un conjunto de fine-tuning puede tener unos pocos cientos de filas, y ahí una sola extracción mal etiquetada supone una parte medible de toda la señal de entrenamiento.

En el pipeline sintético habitual, un generador escribe ejemplos con los hábitos del modelo maestro. Luego un juez los filtra según esos mismos hábitos, y el alumno absorbe los hábitos y sus sesgos. Sin un ancla externa, cada reentrenamiento se acumula sobre el anterior. El modelo aprende a sonar como texto del dominio en lugar de reflejarlo.

Una semilla real corta ese ciclo. Le da al generador un ancla y al juez algo con lo que comparar que no sea su propia salida. El tamaño no es lo importante. Unos pocos cientos de ejemplos sólidos suelen bastar para una tarea de extracción. Lo que cuenta es que cada fila lleve, mediante una extracción que puedas comprobar, a una fuente de la que puedas responder.

El problema es que el material con autoridad suele ser el más difícil de conseguir. Se esconde detrás de renderizado con JavaScript, límites de peticiones por IP y reglas de acceso que frenan a los crawlers estándar.

Qué incluir

Decide el contenido antes de escribir código. Para un modelo de ecommerce, piensa en catálogos de productos y reseñas verificadas de clientes. Un modelo financiero pide informes ante la SEC, transcripciones de presentaciones de resultados y guías regulatorias. Uno médico necesita guías de práctica clínica, fichas técnicas de medicamentos y estudios revisados por pares.

Hay cuatro criterios válidos en cualquier dominio:

  • Con autoridad: confían en ella los profesionales del sector, no solo un buscador.
  • Estructurada: el contenido sigue patrones consistentes a los que puede apuntar un esquema de extracción.
  • Actual: describe el dominio tal como está hoy, no como estaba hace dos años.
  • Relevante: se relaciona directamente con la tarea en lugar de quedar al lado.

Deja fuera las granjas de contenido SEO, el texto generado por IA y todo aquello cuyo origen no sabrías nombrar si alguien te preguntara de dónde salen los datos de entrenamiento.

Cómo obtener páginas protegidas

Una llamada pelada a requests.get() envía una huella TLS simple y ninguna cabecera de navegador, y los sitios protegidos lo detectan enseguida. Si el destino se renderiza con JavaScript, la respuesta es un cascarón vacío.

Una API de fetch puede encargarse del renderizado, del trabajo de acceso y de la conversión a Markdown en una sola petición. En el ejemplo, mode=auto activa el sigilo adaptativo: el servicio alterna renderizado JavaScript y escalada de proxy en cada petición y se queda con la configuración que necesite el destino.

import os
import requests
from dotenv import load_dotenv

load_dotenv()

apikey = os.getenv("ZENROWS_API_KEY")
url = "https://www.scrapingcourse.com/ecommerce/"

params = {
    "url": url,
    "apikey": apikey,
    "mode": "auto",
    "response_type": "markdown",
}

response = requests.get("https://api.zenrows.com/v1/", params=params)
print(response.text)

Lo que devuelve es más pequeño y limpio que el HTML en bruto, porque la mayor parte de la navegación, el texto del pie y el relleno ya han desaparecido. Menos ruido a la entrada significa menos ruido del que aprende el modelo.

PDF y otras fuentes que no son HTML

Muchas de las mejores fuentes de dominio no son páginas web. Fichas técnicas de medicamentos, documentos regulatorios, guías clínicas y normas técnicas se distribuyen como PDF. Para ellos, indica response_type=pdf y además js_render=true, que es obligatorio para respuestas en PDF.

params = {
    "url": url,
    "apikey": apikey,
    "js_render": "true",
    "response_type": "pdf",
}

response = requests.get("https://api.zenrows.com/v1/", params=params)

with open("source-document.pdf", "wb") as f:
    f.write(response.content)

El servicio entrega el archivo, pero no extrae el texto que contiene. Sácalo con pypdf o pdfplumber y luego divídelo en fragmentos antes de que nada de eso se convierta en ejemplo de entrenamiento. Corta siguiendo la estructura y no un número fijo de caracteres. Los propios títulos y secciones del documento son límites naturales, así que cada fragmento conserva su contexto y ningún argumento se parte por la mitad.

Registrar la procedencia

Una semilla con autoridad solo sigue teniéndola si puedes decir de dónde vino cada fila. Añade un campo de metadatos con la URL de origen, la fecha de obtención y la licencia, y el conjunto de datos seguirá siendo auditable y defendible.

source = {
    "url": pdf_url,
    "retrieved": date.today().isoformat(),
    "license": license_url,
}

Esto importa sobre todo cuando alguien pone en duda las respuestas del modelo. Querrás poder decir qué fila salió de qué fuente y si esa fuente permite este uso.

Tres formas de una fila de entrenamiento

Un ejemplo de fine-tuning puede adoptar tres formas, y cada una enseña algo distinto.

Los pares instrucción-respuesta son la opción habitual para el fine-tuning supervisado. Cada ejemplo une el Markdown obtenido de una página con el resultado estructurado correcto, así que el modelo aprende qué extraer y en qué formato devolverlo.

def to_training_example(instruction, markdown_content, extracted_json):
    return {
        "messages": [
            {"role": "system", "content": instruction},
            {"role": "user", "content": markdown_content},
            {"role": "assistant", "content": json.dumps(extracted_json)},
        ]
    }

Los pares de preferencia sirven para DPO y RLHF. Contienen dos salidas para la misma entrada, con una marcada como preferida. Úsalos para empujar a un modelo que ya da resultados decentes hacia otros mejores.

def to_preference_pair(prompt, chosen, rejected):
    return {
        "prompt": prompt,
        "chosen": chosen,
        "rejected": rejected,
    }

La continuación de texto de dominio es texto del dominio sin más, sin separar entrada y salida. Familiariza al modelo con el vocabulario y el estilo, pero se queda corta para extracción, porque el modelo nunca ve qué hay que sacar.

Para tareas de extracción, por tanto, los pares instrucción-respuesta son los que mejor encajan. Las otras dos formas sirven a otros objetivos.

Filtrar antes de que algo entre en la semilla

No toda página scrapeada merece un sitio en el conjunto de datos. Cada fila tiene que superar antes tres comprobaciones:

def keep(markdown, extracted):
    if len(markdown.strip()) < 200:
        return False
    try:
        validate(instance=extracted, schema=schema)
    except ValidationError:
        return False
    h = hashlib.sha256(" ".join(markdown.lower().split()).encode()).hexdigest()
    if h in seen:
        return False
    seen.add(h)
    return True

La primera descarta páginas que salieron casi en blanco tras el renderizado. La segunda, una comprobación de esquema, rechaza tipos incorrectos y campos vacíos. La tercera compara hashes del contenido y marca copias exactas y copias que solo difieren en el formato. Una página tiene que superarlas todas para llegar al entrenamiento.

Cómo medir si funcionó

Antes de que entren los datos sintéticos, separa entre el 10 y el 20 por ciento de la semilla real y no entrenes nunca con ella. Cuando termine el fine-tuning, pasa el modelo por ese holdout y cuenta las salidas que coinciden con la respuesta esperada.

random.seed(42)
random.shuffle(seed_examples)

split = int(len(seed_examples) * 0.85)
train_examples = seed_examples[:split]
holdout_examples = seed_examples[split:]

Repite la comprobación tras cada reentrenamiento y registra la puntuación a lo largo del tiempo. Una cifra estable o en aumento indica que la semilla funciona. Una que cae es un aviso temprano de deriva, visible antes de que se acumule.

Puntos a recordar

  • Una semilla de unos pocos cientos de ejemplos reales de calidad vale más que un gran conjunto sintético que carezca de ella.
  • Las fuentes con autoridad casi siempre están tras controles de acceso que los crawlers estándar no pueden superar.
  • El modo adaptativo se ocupa del renderizado JavaScript y de la detección de bots en cada petición, sin configuración por sitio.
  • Divide los PDF por estructura y no por número de caracteres para que cada fragmento siga siendo coherente.
  • Revisa cada fila al entrar, porque una sola extracción mala pesa mucho en un conjunto de entrenamiento pequeño.
  • Reserva una parte de los datos reales antes de cualquier crecimiento sintético y evalúa después el modelo ajustado con ella.

Para recopilar a mayor escala, el siguiente paso natural es un endpoint por lotes que gestione la concurrencia y los reintentos sobre listas largas de URL.