En 04-04 aprendiste a convertir secuencias en ventanas deslizantes para predecir «el siguiente valor», y quedó prometido que ese mismo truco serviría para generar texto. Es la hora. En este segundo proyecto del módulo construirás un generador de borradores de descripciones de producto para los vendedores de TecnoMarket: un modelo que, carácter a carácter, aprende el estilo de las fichas de producto y propone texto nuevo. Además de las fases habituales del proyecto, esta lección introduce la única pieza técnica nueva que necesitas: el muestreo con temperatura, que controla el equilibrio entre texto seguro-pero-repetitivo y texto creativo-pero-caótico.

Contenido

  1. Enunciado del proyecto y enfoque carácter a carácter
  2. Fase 1: el corpus de descripciones
  3. Fase 2: vectorización a nivel de carácter y ventanas de secuencias
  4. Fase 3: modelo Embedding + LSTM
  5. Fase 4: entrenamiento
  6. Fase 5: muestreo con temperatura
  7. Fase 6: generación iterativa y resultados típicos
  8. Límites del enfoque y uso responsable

Enunciado del proyecto y enfoque carácter a carácter

Contexto de negocio. Los vendedores de TecnoMarket tardan en redactar las descripciones de sus artículos y muchas quedan vacías o telegráficas. Queremos un asistente que genere un borrador con el estilo de la casa, que el vendedor edita y aprueba. El humano siempre firma el texto final.

¿Por qué carácter a carácter? Es la formulación más simple y didáctica de la generación: el modelo ve una secuencia de caracteres y predice cuál viene después. Es exactamente el problema de 04-04 (predecir el siguiente paso de una serie) cambiando números por caracteres: mismas ventanas deslizantes, misma cabeza de clasificación que en 04-03. Con vocabularios de ~50 símbolos no hay que gestionar palabras desconocidas y el modelo cabe en cualquier máquina. El precio — coherencia limitada a frases cortas — lo analizaremos al final.

Fase 1: el corpus de descripciones

Para el prototipo usamos un corpus sintético de descripciones ficticias de TecnoMarket. En un caso real usarías el histórico de fichas aprobadas; aquí generamos material de estilo homogéneo (también puedes practicar con cualquier texto público en español, pero el corpus propio mantiene el hilo del proyecto):

import numpy as np
import tensorflow as tf

tf.random.set_seed(42)  # 06-04: reproducibilidad

productos = ["auriculares inalambricos", "teclado mecanico", "monitor curvo",
             "robot aspirador", "freidora de aire", "camara de seguridad",
             "altavoz bluetooth", "cafetera espresso", "raton gaming",
             "lampara inteligente", "bascula digital", "ventilador de torre"]
adjetivos = ["compacto", "potente", "silencioso", "elegante", "versatil",
             "ergonomico", "resistente", "ligero"]
ventajas = ["bateria de larga duracion", "conexion estable", "diseno moderno",
            "facil instalacion", "bajo consumo", "gran capacidad",
            "control desde el movil", "materiales de primera calidad"]
cierres = ["ideal para el hogar.", "perfecto para tu dia a dia.",
           "la mejor opcion calidad precio.", "envio en 24 horas."]

rng = np.random.default_rng(42)
frases = []
for _ in range(3000):
    p, a = rng.choice(productos), rng.choice(adjetivos)
    v1, v2 = rng.choice(ventajas, size=2, replace=False)
    c = rng.choice(cierres)
    frases.append(f"{p} {a} con {v1} y {v2}. {c}\n")

corpus = "".join(frases)
print(len(corpus))            # ~300 000 caracteres
print(corpus[:200])

Observa dos decisiones: el corpus está en minúsculas y sin tildes (vocabulario pequeño y limpio) y cada descripción termina en \n, que el modelo aprenderá como señal de «fin de descripción». Unos 300 000 caracteres son pocos para generación de calidad, pero suficientes para que el modelo aprenda el estilo — calibra expectativas en consecuencia.

Fase 2: vectorización a nivel de carácter y ventanas de secuencias

En 04-03 usamos TextVectorization a nivel de palabra; aquí bajamos al carácter con StringLookup, y aplicamos las ventanas deslizantes de 04-04 con tf.data (06-01):

vocab = sorted(set(corpus))
print(len(vocab))   # ~45 símbolos: letras, dígitos, espacio, puntuación, \n

char_a_id = tf.keras.layers.StringLookup(vocabulary=vocab, mask_token=None)
id_a_char = tf.keras.layers.StringLookup(vocabulary=vocab, invert=True,
                                         mask_token=None)

ids = char_a_id(tf.strings.unicode_split(corpus, "UTF-8"))

LONG = 80   # longitud de ventana: ~una descripción completa
ds = tf.data.Dataset.from_tensor_slices(ids)
ds = ds.batch(LONG + 1, drop_remainder=True)   # trozos de 81 caracteres

def entrada_objetivo(trozo):
    return trozo[:-1], trozo[1:]   # entrada: 80 chars; objetivo: los mismos desplazados 1

BATCH = 64
train_ds = (ds.map(entrada_objetivo)
              .shuffle(10_000)
              .batch(BATCH, drop_remainder=True)
              .prefetch(tf.data.AUTOTUNE))

La clave está en entrada_objetivo: para la entrada "teclado mecanic", el objetivo es "eclado mecanico" — en cada posición temporal el modelo aprende a predecir el carácter siguiente. Es la misma idea de ventana→siguiente-paso de 04-04, pero entrenando las 80 posiciones de la ventana a la vez, lo que multiplica la señal de aprendizaje por ejemplo.

Fase 3: modelo Embedding + LSTM

La arquitectura reutiliza las piezas de 04-02 y 04-03: un Embedding (aquí de caracteres, no de palabras) y una LSTM con return_sequences=True, porque necesitamos una predicción en cada paso temporal, no solo al final:

from tensorflow.keras import layers, models

VOCAB = len(char_a_id.get_vocabulary())   # incluye el token [UNK]

modelo = models.Sequential([
    layers.Embedding(VOCAB, 64),                     # cada char -> vector de 64
    layers.LSTM(256, return_sequences=True),         # estado por cada posición
    layers.Dropout(0.2),                             # regularización (05-04)
    layers.Dense(VOCAB),                             # logits: puntuación por char
])

modelo.compile(
    optimizer="adam",
    loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
)
modelo.build(input_shape=(None, None))
modelo.summary()   # ~0.4 M de parámetros

Dos detalles importantes:

  • La capa final no lleva softmax (from_logits=True en la pérdida): en la fase de muestreo manipularemos los logits directamente para aplicar la temperatura.
  • La pérdida es la CCE de siempre (02-04) aplicada por paso temporal: Keras promedia la entropía cruzada de las 80 predicciones de cada ventana.

Fase 4: entrenamiento

callbacks = [
    tf.keras.callbacks.ModelCheckpoint("models/generador_mejor.keras",
                                       monitor="loss", save_best_only=True),
    tf.keras.callbacks.EarlyStopping(monitor="loss", patience=3,
                                     restore_best_weights=True),
]
historia = modelo.fit(train_ds, epochs=30, callbacks=callbacks)

Guía de lectura de la pérdida (con vocabulario de ~45 símbolos, la pérdida inicial ronda ln(45) ≈ 3.8 — predicción aleatoria):

Pérdida Qué genera el modelo
~3.8 Sopa de caracteres aleatorios
~2.0 Pseudo-palabras pronunciables, espacios en su sitio
~1.2 Palabras reales del corpus, sintaxis aproximada
< 0.8 Frases con la estructura de las descripciones

Con este corpus sintético (muy regular) la pérdida baja rápido, hacia 0.5-0.7 en 15-25 épocas y pocos minutos de CPU. Con un corpus real, más variado, esperarías valores más altos y más tiempo. No monitorizamos validación aquí: en generación de prototipo nos interesa capturar el estilo del corpus, y la evaluación decisiva será cualitativa (fase 6).

Fase 5: muestreo con temperatura

Entrenado el modelo, ¿cómo elegimos el siguiente carácter a partir de sus logits? Aquí aparece el concepto central de la lección.

  • Greedy (voraz): elegir siempre el carácter más probable. Determinista y «seguro», pero cae en bucles: con bateria de larga duracion y bateria de larga duracion y ....
  • Muestreo con temperatura: dividir los logits entre una temperatura T antes del softmax y muestrear de la distribución resultante:
def siguiente_char(logits, temperatura):
    logits = logits / temperatura
    id_muestra = tf.random.categorical(logits[tf.newaxis, :], num_samples=1)
    return int(id_muestra[0, 0])

Efecto de T sobre la distribución:

Temperatura Efecto Salida típica
T → 0 Casi greedy: gana siempre el más probable Correcta pero repetitiva, frases clonadas del corpus
T = 0.5 Conservadora: reparte poco Buen compromiso por defecto
T = 1.0 Distribución tal cual la aprendió el modelo Variada, algún tropiezo
T = 1.5 Aplana la distribución: los improbables ganan opciones Inventa palabras: freidora de aire vertatil con conexo estible

La temperatura no cambia el modelo: cambia cuánto riesgo aceptas al muestrear. Para borradores comerciales, temperaturas bajas-medias (0.4-0.8) son lo razonable.

Fase 6: generación iterativa y resultados típicos

Generar es iterar: predecir un carácter, añadirlo a la secuencia, volver a predecir. Versión didáctica (re-procesa toda la secuencia en cada paso; suficiente para un prototipo):

def generar(semilla, n_chars=150, temperatura=0.5):
    ids_gen = char_a_id(tf.strings.unicode_split(semilla, "UTF-8"))
    ids_gen = list(ids_gen.numpy())
    for _ in range(n_chars):
        entrada = tf.constant([ids_gen[-LONG:]])       # última ventana
        logits = modelo(entrada)[0, -1, :]             # logits del ultimo paso
        ids_gen.append(siguiente_char(logits, temperatura))
    chars = id_a_char(tf.constant(ids_gen))
    return tf.strings.reduce_join(chars).numpy().decode("utf-8")

print(generar("robot aspirador ", temperatura=0.5))

Salidas típicas reales de un modelo así entrenado (las tuyas variarán):

  • T=0.2: robot aspirador silencioso con bateria de larga duracion y facil instalacion. ideal para el hogar. — impecable, pero casi calcada del corpus.
  • T=0.7: robot aspirador compacto con control desde el movil y bajo consumo. la mejor opcion calidad precio. — combina piezas de forma nueva y correcta: el punto dulce.
  • T=1.4: robot aspirador ergonimo con gron combacidad y conexion estoble. envio en 24 hores. — creatividad desbocada: errores ortográficos y sintaxis rota.

Entrega. Igual que en 07-01, el generador se empaqueta con su preprocesado (el StringLookup viaja con el modelo si lo integras en un modelo de inferencia, 06-05) y se serviría tras un endpoint FastAPI (POST /borrador con producto y temperatura) que devuelve 3 borradores para que el vendedor elija y edite.

Límites del enfoque y uso responsable

Sé honesto con lo que has construido:

  • Coherencia corta: una LSTM de caracteres mantiene el hilo unas decenas de caracteres; en textos largos se contradice o divaga. Nuestro corpus de frases cortas encaja justo dentro de ese límite — por eso funciona.
  • No sabe de hechos: puede escribir «bateria de larga duracion» para un producto sin batería. Genera estilo, no verdad.
  • Los sistemas comerciales de generación usan transformers/LLMs basados en la atención que viste en 05-05: contexto de miles de tokens y conocimiento general. El mecanismo de muestreo con temperatura que has aprendido aquí es exactamente el mismo que usan ellos — este proyecto te ha enseñado el motor en pequeño.
  • Uso responsable en TecnoMarket: el modelo produce borradores; un humano revisa, corrige la ficha técnica y aprueba antes de publicar. Es la misma filosofía de la cola de revisión de 03-04: automatizar lo mecánico, supervisar lo que llega al cliente.

Errores Comunes y Consejos

  • Olvidar return_sequences=True: la LSTM devolvería solo el último estado y las formas no cuadrarían con el objetivo de 80 posiciones. Repasa 04-02 si el error de formas te despista.
  • Poner softmax en la última capa y además from_logits=True: doble softmax silencioso que degrada el entrenamiento. Elige uno de los dos; aquí, logits.
  • Evaluar la generación solo por la pérdida: una pérdida baja con un corpus repetitivo puede significar pura memorización. Genera y lee: la inspección cualitativa es parte de la evaluación.
  • Usar temperatura alta «para más creatividad» en producción: en textos comerciales, las palabras inventadas destruyen la confianza del cliente. Empieza en 0.5 y ajusta con ejemplos delante.
  • Ventanas más largas que las descripciones sin motivo: alargan el entrenamiento sin mejorar un corpus de frases cortas. Ajusta LONG al texto real.

Ejercicios

  1. Entrena el mismo modelo con LSTM(128) y con LSTM(512) y compara pérdida final, tiempo por época y calidad de tres muestras a T=0.7. ¿Dónde están los rendimientos decrecientes?
  2. Añade al corpus un 10 % de descripciones con un formato nuevo (por ejemplo, empezando por «oferta: »). Reentrena y comprueba con qué frecuencia y fidelidad el modelo genera ese formato.
  3. Implementa generar_lote(semilla, k, temperatura) que devuelva k borradores distintos y descarte los que contengan palabras fuera de un diccionario del corpus (control de calidad automático previo a la revisión humana).

Soluciones

  1. Con 128 unidades la pérdida se queda típicamente 0.1-0.2 por encima y aparecen más tropiezos ortográficos; con 512 la pérdida mejora poco (el corpus es simple) y el tiempo por época se multiplica por ~3. Con este corpus, 256 es el punto dulce: más capacidad no aporta porque no hay más complejidad que aprender.
  2. Basta añadir frases.append(f"oferta: {p} {a} con {v1}. {c}\n") dentro de un bucle adicional (~300 frases). Tras reentrenar, semillas que empiecen por "oferta: " completan el formato de forma consistente; sin esa semilla, el patrón aparece espontáneamente en torno al 10 % de las muestras — el modelo reproduce las frecuencias del corpus.
  3. Construye el diccionario con palabras_validas = set(corpus.split()); genera en bucle con generar, divide cada borrador con .split() y acepta solo si all(p.strip('.') in palabras_validas for p in borrador.split()). Devuelve los k primeros aceptados (con un tope de intentos). Este filtro barato elimina la mayoría de artefactos de temperaturas altas antes de que los vea el vendedor.

Conclusión

Segundo proyecto entregado: un generador de borradores que aprende el estilo de TecnoMarket carácter a carácter, con las ventanas deslizantes de 04-04, el tándem Embedding+LSTM de 04-03 y un muestreo con temperatura que ahora sabes leer y ajustar. Igual de valioso es lo que sabes que no hace: coherencia larga y veracidad quedan para los transformers de 05-05, y todo texto generado pasa por revisión humana. En la próxima lección cambiamos de nuevo de problema y cumplimos la promesa de 05-02: usar el error de reconstrucción de un autoencoder para detectar transacciones fraudulentas en TecnoMarket — nuestro primer proyecto con datos desbalanceados de verdad.

Curso de Deep Learning

Módulo 1: Introducción a Deep Learning

Módulo 2: Fundamentos de Redes Neuronales

Módulo 3: Redes Neuronales Convolucionales (CNN)

Módulo 4: Redes Neuronales Recurrentes (RNN)

Módulo 5: Técnicas Avanzadas en Deep Learning

Módulo 6: Herramientas y Frameworks

Módulo 7: Proyectos Prácticos

Módulo 8: Consideraciones Éticas y Futuro del Deep Learning

© Copyright 2026. Todos los derechos reservados