A 04-04 vas aprendre a convertir seqüències en finestres lliscants per predir «el valor següent», i va quedar promès que aquest mateix truc serviria per generar text. Ha arribat l'hora. En aquest segon projecte del mòdul construiràs un generador d'esborranys de descripcions de producte per als venedors de TecnoMarket: un model que, caràcter a caràcter, aprèn l'estil de les fitxes de producte i proposa text nou. A més de les fases habituals del projecte, aquesta lliçó introdueix l'única peça tècnica nova que necessites: el mostreig amb temperatura, que controla l'equilibri entre text segur-però-repetitiu i text creatiu-però-caòtic.

Contingut

  1. Enunciat del projecte i enfocament caràcter a caràcter
  2. Fase 1: el corpus de descripcions
  3. Fase 2: vectorització a nivell de caràcter i finestres de seqüències
  4. Fase 3: model Embedding + LSTM
  5. Fase 4: entrenament
  6. Fase 5: mostreig amb temperatura
  7. Fase 6: generació iterativa i resultats típics
  8. Límits de l'enfocament i ús responsable

Enunciat del projecte i enfocament caràcter a caràcter

Context de negoci. Els venedors de TecnoMarket triguen a redactar les descripcions dels seus articles i moltes queden buides o telegràfiques. Volem un assistent que generi un esborrany amb l'estil de la casa, que el venedor edita i aprova. L'humà sempre signa el text final.

Per què caràcter a caràcter? És la formulació més simple i didàctica de la generació: el model veu una seqüència de caràcters i prediu quin ve després. És exactament el problema de 04-04 (predir el pas següent d'una sèrie) canviant números per caràcters: mateixes finestres lliscants, mateix cap de classificació que a 04-03. Amb vocabularis de ~50 símbols no cal gestionar paraules desconegudes i el model cap a qualsevol màquina. El preu — coherència limitada a frases curtes — l'analitzarem al final.

Fase 1: el corpus de descripcions

Per al prototip fem servir un corpus sintètic de descripcions fictícies de TecnoMarket. En un cas real faries servir l'històric de fitxes aprovades; aquí generem material d'estil homogeni (també pots practicar amb qualsevol text públic en català, però el corpus propi manté el fil del projecte):

import numpy as np
import tensorflow as tf

tf.random.set_seed(42)  # 06-04: reproduïbilitat

productes = ["auriculars sense fils", "teclat mecanic", "monitor corbat",
             "robot aspirador", "fregidora d'aire", "camera de seguretat",
             "altaveu bluetooth", "cafetera espresso", "ratoli gaming",
             "lampada intelligent", "bascula digital", "ventilador de torre"]
adjectius = ["compacte", "potent", "silencios", "elegant", "versatil",
             "ergonomic", "resistent", "lleuger"]
avantatges = ["bateria de llarga durada", "connexio estable", "disseny modern",
              "installacio facil", "baix consum", "gran capacitat",
              "control des del mobil", "materials de primera qualitat"]
tancaments = ["ideal per a la llar.", "perfecte per al teu dia a dia.",
              "la millor opcio qualitat preu.", "enviament en 24 hores."]

rng = np.random.default_rng(42)
frases = []
for _ in range(3000):
    p, a = rng.choice(productes), rng.choice(adjectius)
    v1, v2 = rng.choice(avantatges, size=2, replace=False)
    c = rng.choice(tancaments)
    frases.append(f"{p} {a} amb {v1} i {v2}. {c}\n")

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

Fixa't en dues decisions: el corpus està en minúscules i sense accents ni ela geminada (vocabulari petit i net) i cada descripció acaba en \n, que el model aprendrà com a senyal de «fi de descripció». Uns 300 000 caràcters són pocs per a generació de qualitat, però suficients perquè el model n'aprengui l'estil — calibra les expectatives en conseqüència.

Fase 2: vectorització a nivell de caràcter i finestres de seqüències

A 04-03 vam fer servir TextVectorization a nivell de paraula; aquí baixem al caràcter amb StringLookup, i apliquem les finestres lliscants de 04-04 amb tf.data (06-01):

vocab = sorted(set(corpus))
print(len(vocab))   # ~45 símbols: lletres, dígits, espai, puntuació, \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 finestra: ~una descripció completa
ds = tf.data.Dataset.from_tensor_slices(ids)
ds = ds.batch(LONG + 1, drop_remainder=True)   # trossos de 81 caràcters

def entrada_objectiu(tros):
    return tros[:-1], tros[1:]   # entrada: 80 chars; objectiu: els mateixos desplaçats 1

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

La clau és a entrada_objectiu: per a l'entrada "teclat mecani", l'objectiu és "eclat mecanic"a cada posició temporal el model aprèn a predir el caràcter següent. És la mateixa idea de finestra→pas-següent de 04-04, però entrenant les 80 posicions de la finestra alhora, cosa que multiplica el senyal d'aprenentatge per exemple.

Fase 3: model Embedding + LSTM

L'arquitectura reutilitza les peces de 04-02 i 04-03: un Embedding (aquí de caràcters, no de paraules) i una LSTM amb return_sequences=True, perquè necessitem una predicció a cada pas temporal, no només al final:

from tensorflow.keras import layers, models

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

model = models.Sequential([
    layers.Embedding(VOCAB, 64),                     # cada char -> vector de 64
    layers.LSTM(256, return_sequences=True),         # estat per cada posició
    layers.Dropout(0.2),                             # regularització (05-04)
    layers.Dense(VOCAB),                             # logits: puntuació per char
])

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

Dos detalls importants:

  • La capa final no porta softmax (from_logits=True a la pèrdua): a la fase de mostreig manipularem els logits directament per aplicar-hi la temperatura.
  • La pèrdua és la CCE de sempre (02-04) aplicada per pas temporal: Keras fa la mitjana de l'entropia creuada de les 80 prediccions de cada finestra.

Fase 4: entrenament

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

Guia de lectura de la pèrdua (amb un vocabulari de ~45 símbols, la pèrdua inicial ronda ln(45) ≈ 3.8 — predicció aleatòria):

Pèrdua Què genera el model
~3.8 Sopa de caràcters aleatoris
~2.0 Pseudo-paraules pronunciables, espais al seu lloc
~1.2 Paraules reals del corpus, sintaxi aproximada
< 0.8 Frases amb l'estructura de les descripcions

Amb aquest corpus sintètic (molt regular) la pèrdua baixa ràpid, cap a 0.5-0.7 en 15-25 èpoques i pocs minuts de CPU. Amb un corpus real, més variat, esperaries valors més alts i més temps. Aquí no monitorem validació: en generació de prototip ens interessa capturar l'estil del corpus, i l'avaluació decisiva serà qualitativa (fase 6).

Fase 5: mostreig amb temperatura

Entrenat el model, com triem el caràcter següent a partir dels seus logits? Aquí apareix el concepte central de la lliçó.

  • Greedy (voraç): triar sempre el caràcter més probable. Determinista i «segur», però cau en bucles: amb bateria de llarga durada i bateria de llarga durada i ....
  • Mostreig amb temperatura: dividir els logits per una temperatura T abans del softmax i mostrejar de la distribució resultant:
def seguent_char(logits, temperatura):
    logits = logits / temperatura
    id_mostra = tf.random.categorical(logits[tf.newaxis, :], num_samples=1)
    return int(id_mostra[0, 0])

Efecte de T sobre la distribució:

Temperatura Efecte Sortida típica
T → 0 Gairebé greedy: guanya sempre el més probable Correcta però repetitiva, frases clonades del corpus
T = 0.5 Conservadora: reparteix poc Bon compromís per defecte
T = 1.0 Distribució tal com la va aprendre el model Variada, alguna ensopegada
T = 1.5 Aplana la distribució: els improbables guanyen opcions Inventa paraules: fregidora d'aire vertatil amb connexo estible

La temperatura no canvia el model: canvia quant risc acceptes en mostrejar. Per a esborranys comercials, temperatures baixes-mitjanes (0.4-0.8) són el que és raonable.

Fase 6: generació iterativa i resultats típics

Generar és iterar: predir un caràcter, afegir-lo a la seqüència, tornar a predir. Versió didàctica (re-processa tota la seqüència a cada pas; suficient per a un prototip):

def generar(llavor, n_chars=150, temperatura=0.5):
    ids_gen = char_a_id(tf.strings.unicode_split(llavor, "UTF-8"))
    ids_gen = list(ids_gen.numpy())
    for _ in range(n_chars):
        entrada = tf.constant([ids_gen[-LONG:]])       # última finestra
        logits = model(entrada)[0, -1, :]              # logits de l'últim pas
        ids_gen.append(seguent_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))

Sortides típiques reals d'un model entrenat així (les teves variaran):

  • T=0.2: robot aspirador silencios amb bateria de llarga durada i installacio facil. ideal per a la llar. — impecable, però gairebé calcada del corpus.
  • T=0.7: robot aspirador compacte amb control des del mobil i baix consum. la millor opcio qualitat preu. — combina peces d'una manera nova i correcta: el punt dolç.
  • T=1.4: robot aspirador ergonamic amb gron copocitat i connexo estoble. enviament en 24 hares. — creativitat desbocada: errors ortogràfics i sintaxi trencada.

Lliurament. Igual que a 07-01, el generador s'empaqueta amb el seu preprocessament (l'StringLookup viatja amb el model si l'integres en un model d'inferència, 06-05) i se serviria darrere d'un endpoint FastAPI (POST /esborrany amb producte i temperatura) que retorna 3 esborranys perquè el venedor triï i editi.

Límits de l'enfocament i ús responsable

Sigues honest amb el que has construït:

  • Coherència curta: una LSTM de caràcters manté el fil unes desenes de caràcters; en textos llargs es contradiu o divaga. El nostre corpus de frases curtes encaixa just dins d'aquest límit — per això funciona.
  • No sap de fets: pot escriure «bateria de llarga durada» per a un producte sense bateria. Genera estil, no veritat.
  • Els sistemes comercials de generació fan servir transformers/LLMs basats en l'atenció que vas veure a 05-05: context de milers de tokens i coneixement general. El mecanisme de mostreig amb temperatura que has après aquí és exactament el mateix que fan servir ells — aquest projecte t'ha ensenyat el motor en petit.
  • Ús responsable a TecnoMarket: el model produeix esborranys; un humà revisa, corregeix la fitxa tècnica i aprova abans de publicar. És la mateixa filosofia de la cua de revisió de 03-04: automatitzar el que és mecànic, supervisar el que arriba al client.

Errors Comuns i Consells

  • Oblidar return_sequences=True: la LSTM retornaria només l'últim estat i les formes no quadrarien amb l'objectiu de 80 posicions. Repassa 04-02 si l'error de formes et desconcerta.
  • Posar softmax a l'última capa i a més from_logits=True: doble softmax silenciós que degrada l'entrenament. Tria un dels dos; aquí, logits.
  • Avaluar la generació només per la pèrdua: una pèrdua baixa amb un corpus repetitiu pot significar pura memorització. Genera i llegeix: la inspecció qualitativa és part de l'avaluació.
  • Fer servir temperatura alta «per a més creativitat» en producció: en textos comercials, les paraules inventades destrueixen la confiança del client. Comença a 0.5 i ajusta amb exemples al davant.
  • Finestres més llargues que les descripcions sense motiu: allarguen l'entrenament sense millorar un corpus de frases curtes. Ajusta LONG al text real.

Exercicis

  1. Entrena el mateix model amb LSTM(128) i amb LSTM(512) i compara pèrdua final, temps per època i qualitat de tres mostres a T=0.7. On són els rendiments decreixents?
  2. Afegeix al corpus un 10 % de descripcions amb un format nou (per exemple, començant per «oferta: »). Reentrena i comprova amb quina freqüència i fidelitat el model genera aquest format.
  3. Implementa generar_lot(llavor, k, temperatura) que retorni k esborranys diferents i descarti els que continguin paraules fora d'un diccionari del corpus (control de qualitat automàtic previ a la revisió humana).

Solucions

  1. Amb 128 unitats la pèrdua es queda típicament 0.1-0.2 per sobre i apareixen més ensopegades ortogràfiques; amb 512 la pèrdua millora poc (el corpus és simple) i el temps per època es multiplica per ~3. Amb aquest corpus, 256 és el punt dolç: més capacitat no aporta res perquè no hi ha més complexitat per aprendre.
  2. N'hi ha prou d'afegir frases.append(f"oferta: {p} {a} amb {v1}. {c}\n") dins d'un bucle addicional (~300 frases). Després de reentrenar, llavors que comencin per "oferta: " completen el format de manera consistent; sense aquesta llavor, el patró apareix espontàniament al voltant del 10 % de les mostres — el model reprodueix les freqüències del corpus.
  3. Construeix el diccionari amb paraules_valides = set(corpus.split()); genera en bucle amb generar, divideix cada esborrany amb .split() i accepta'l només si all(p.strip('.') in paraules_valides for p in esborrany.split()). Retorna els k primers acceptats (amb un límit d'intents). Aquest filtre barat elimina la majoria d'artefactes de temperatures altes abans que els vegi el venedor.

Conclusió

Segon projecte lliurat: un generador d'esborranys que aprèn l'estil de TecnoMarket caràcter a caràcter, amb les finestres lliscants de 04-04, el tàndem Embedding+LSTM de 04-03 i un mostreig amb temperatura que ara saps llegir i ajustar. Igual de valuós és el que saps que no fa: coherència llarga i veracitat queden per als transformers de 05-05, i tot text generat passa per revisió humana. A la propera lliçó tornem a canviar de problema i complim la promesa de 05-02: fer servir l'error de reconstrucció d'un autoencoder per detectar transaccions fraudulentes a TecnoMarket — el nostre primer projecte amb dades desbalancejades de veritat.

Curs de Deep Learning

Mòdul 1: Introducció al Deep Learning

Mòdul 2: Fonaments de Xarxes Neuronals

Mòdul 3: Xarxes Neuronals Convolucionals (CNN)

Mòdul 4: Xarxes Neuronals Recurrents (RNN)

Mòdul 5: Tècniques Avançades en Deep Learning

Mòdul 6: Eines i Frameworks

Mòdul 7: Projectes Pràctics

Mòdul 8: Consideracions Ètiques i Futur del Deep Learning

© Copyright 2026. Tots els drets reservats