A 05-01 vas muntar l'esquelet d'una DCGAN — generador amb Conv2DTranspose, discriminador convolucional, la metàfora del falsificador i el policia — i vam deixar pendent la part més delicada: el bucle d'entrenament complet. Amb el GradientTape que vas dominar a 06-01, ja tens l'eina exacta per escriure'l. En aquest quart projecte entrenaràs una GAN de cap a cap sobre Fashion-MNIST (peces de roba com a substitut de fotos de producte) com a prototip d'un generador d'imatges per a les creativitats promocionals de TecnoMarket, aprenent a llegir la dinàmica d'un entrenament adversari: què és normal, què és mode collapse i què tocar quan alguna cosa va malament.
Contingut
- Enunciat del projecte i expectatives realistes
- Fase 1: dades — Fashion-MNIST amb
tf.data - Fase 2: generador i discriminador (reprenent 05-01)
- Fase 3: les dues pèrdues adversàries
- Fase 4: un pas d'entrenament amb GradientTape, línia a línia
- Fase 5: el bucle per èpoques amb graelles de mostres
- Fase 6: llegir la dinàmica — el que és normal, el mode collapse i què tocar
- Fase 7: avaluació, desat del generador i ús posterior
Enunciat del projecte i expectatives realistes
Context de negoci. L'equip de màrqueting de TecnoMarket vol explorar la generació d'imatges per a creativitats promocionals (fons, variacions de producte, il·lustracions de campanya). Abans d'invertir, demanen un prototip educatiu que demostri que l'equip domina la mecànica generativa.
Substitut. Fashion-MNIST: 60 000 imatges 28×28 en escala de grisos de peces de roba (samarretes, sabatilles, bosses...), un substitut raonable de fotos de producte simples. Petit, ràpid d'entrenar i prou variat per veure els fenòmens que importen.
Expectatives, per escrit abans de començar: una DCGAN d'aquesta mida produeix peces de roba recognoscibles però borroses a 28×28. No és StyleGAN ni un producte comercial; els sistemes de producció actuals fan servir models de difusió (els veuràs esmentats a 08-03) i tota generació d'imatges comercials arrossega implicacions ètiques que tractarem a 08-01. L'objectiu aquí és dominar el mecanisme adversari — que és també la base conceptual per entendre els sistemes moderns.
Fase 1: dades — Fashion-MNIST amb tf.data
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
tf.random.set_seed(42)
(x_train, _), _ = tf.keras.datasets.fashion_mnist.load_data()
# Normalitzar a [-1, 1]: el generador acabarà en tanh (05-01)
x_train = (x_train.astype("float32") - 127.5) / 127.5
x_train = x_train[..., np.newaxis] # (60000, 28, 28, 1)
BATCH = 128
train_ds = (tf.data.Dataset.from_tensor_slices(x_train)
.shuffle(60_000)
.batch(BATCH, drop_remainder=True)
.prefetch(tf.data.AUTOTUNE))Detall important que ja vam anticipar a 05-01: el rang [-1, 1] (no [0, 1]) perquè la sortida del generador serà tanh. Generador i dades reals han de parlar el mateix idioma numèric o el discriminador els distingirà pel rang, no pel contingut. No hi ha etiquetes ni conjunt de test: en una GAN les «dades» són només la referència de realitat.
Fase 2: generador i discriminador (reprenent 05-01)
Instanciem l'esquelet DCGAN de 05-01 adaptat a 28×28. No en reexpliquem les peces (repassa-les allà); n'anotem les decisions:
from tensorflow.keras import layers, models
DIM_SOROLL = 100
def crear_generador():
return models.Sequential([
layers.Input(shape=(DIM_SOROLL,)),
layers.Dense(7 * 7 * 256, use_bias=False),
layers.BatchNormalization(), layers.LeakyReLU(),
layers.Reshape((7, 7, 256)),
layers.Conv2DTranspose(128, 5, strides=1, padding="same", use_bias=False),
layers.BatchNormalization(), layers.LeakyReLU(), # 7x7
layers.Conv2DTranspose(64, 5, strides=2, padding="same", use_bias=False),
layers.BatchNormalization(), layers.LeakyReLU(), # 14x14
layers.Conv2DTranspose(1, 5, strides=2, padding="same",
activation="tanh"), # 28x28, [-1,1]
], name="generador")
def crear_discriminador():
return models.Sequential([
layers.Input(shape=(28, 28, 1)),
layers.Conv2D(64, 5, strides=2, padding="same"),
layers.LeakyReLU(), layers.Dropout(0.3), # 14x14
layers.Conv2D(128, 5, strides=2, padding="same"),
layers.LeakyReLU(), layers.Dropout(0.3), # 7x7
layers.Flatten(),
layers.Dense(1), # logit: real o fals
], name="discriminador")
generador = crear_generador()
discriminador = crear_discriminador()Recordatoris de 05-01 aplicats: Conv2DTranspose amb strides=2 duplica la resolució (7→14→28), BN i LeakyReLU estabilitzen el generador, el discriminador fa servir dropout (no BN) i retorna un logit sense sigmoide — la pèrdua se n'encarregarà.
Fase 3: les dues pèrdues adversàries
El joc de 05-01, ara en codi. Totes dues pèrdues parteixen de la mateixa entropia creuada binària sobre logits:
bce = tf.keras.losses.BinaryCrossentropy(from_logits=True)
def perdua_discriminador(logits_reals, logits_falsos):
# El policia vol: reals -> 1, falses -> 0
perdua_real = bce(tf.ones_like(logits_reals), logits_reals)
perdua_falsa = bce(tf.zeros_like(logits_falsos), logits_falsos)
return perdua_real + perdua_falsa
def perdua_generador(logits_falsos):
# El falsificador vol que les SEVES falses semblin reals (etiqueta 1)
return bce(tf.ones_like(logits_falsos), logits_falsos)
# Dos optimitzadors separats: cada xarxa aprèn pel seu compte (05-01)
opt_gen = tf.keras.optimizers.Adam(2e-4, beta_1=0.5)
opt_disc = tf.keras.optimizers.Adam(2e-4, beta_1=0.5)L'asimetria és l'essència: el discriminador puntua les mateixes imatges falses amb objectiu 0 i el generador amb objectiu 1. Els hiperparàmetres lr=2e-4, beta_1=0.5 són els clàssics de DCGAN — funcionen; no els toquis a la primera passada.
Fase 4: un pas d'entrenament amb GradientTape, línia a línia
Aquí s'ajunta tot: el GradientTape de 06-01 ens permet calcular dos conjunts de gradients d'un mateix graf i aplicar-los a xarxes diferents. Comentat línia a línia:
@tf.function # compila el pas a graf: ~5-10x més ràpid (06-01)
def pas_entrenament(imatges_reals):
soroll = tf.random.normal([BATCH, DIM_SOROLL]) # 1. lot de soroll
with tf.GradientTape() as tape_g, tf.GradientTape() as tape_d:
imatges_falses = generador(soroll, training=True) # 2. falsificar
logits_reals = discriminador(imatges_reals, training=True) # 3. jutjar reals
logits_falsos = discriminador(imatges_falses, training=True) # 4. jutjar falses
perd_g = perdua_generador(logits_falsos) # 5. quant enganya
perd_d = perdua_discriminador(logits_reals, logits_falsos) # 6. quant encerta
# 7. Gradients de la pèrdua de CADA xarxa respecte a les SEVES variables
grads_g = tape_g.gradient(perd_g, generador.trainable_variables)
grads_d = tape_d.gradient(perd_d, discriminador.trainable_variables)
# 8. Cada optimitzador actualitza només la seva xarxa
opt_gen.apply_gradients(zip(grads_g, generador.trainable_variables))
opt_disc.apply_gradients(zip(grads_d, discriminador.trainable_variables))
return perd_g, perd_dPunts fins que has d'entendre, no només copiar:
- Dos tapes, una passada: tots dos graven les mateixes operacions, però el pas 7 demana a cada tape gradients només de la seva pèrdua respecte a les seves variables. En actualitzar el generador no toquem el discriminador, i viceversa — si barregessis variables, cada xarxa sabotejaria l'altra.
training=Truea totes dues xarxes sempre: la BN del generador ha de fer servir estadístiques del lot també quan les seves imatges alimenten la pèrdua del discriminador.@tf.function: el mateix decorador que vam veure a 06-01; en un bucle personalitzat com aquest, la diferència de velocitat és enorme.- Aquest és el patró que Keras
fit()no et dona fet: dues xarxes, dues pèrdues enfrontades, un pas simultani. Per això la promesa de 05-01 va haver d'esperar fins a 06-01.
Fase 5: el bucle per èpoques amb graelles de mostres
En una GAN, la pèrdua no explica tota la veritat (fase 6), així que el bucle genera periòdicament una graella de mostres amb el mateix soroll fix, per comparar èpoques en igualtat de condicions:
soroll_fix = tf.random.normal([16, DIM_SOROLL], seed=42) # SEMPRE el mateix
def desar_graella(epoca):
mostres = generador(soroll_fix, training=False)
mostres = (mostres + 1) / 2 # [-1,1] -> [0,1] per pintar
plt.figure(figsize=(4, 4))
for i in range(16):
plt.subplot(4, 4, i + 1)
plt.imshow(mostres[i, :, :, 0], cmap="gray")
plt.axis("off")
plt.savefig(f"logs/gan/graella_epoca_{epoca:03d}.png")
plt.close()
EPOQUES = 50
for epoca in range(1, EPOQUES + 1):
perdues_g, perdues_d = [], []
for lot in train_ds:
pg, pd = pas_entrenament(lot)
perdues_g.append(float(pg)); perdues_d.append(float(pd))
print(f"Època {epoca:3d} | G: {np.mean(perdues_g):.3f} "
f"| D: {np.mean(perdues_d):.3f}")
if epoca % 5 == 0 or epoca == 1:
desar_graella(epoca)
generador.save(f"models/generador_epoca_{epoca:03d}.keras")En GPU, cada època triga ~20-40 s (50 èpoques ≈ mitja hora); en CPU és lent — redueix a 15-20 èpoques o fes servir Colab (01-05). Desem checkpoints del generador cada 5 èpoques: en les GAN, la «millor època» es tria mirant graelles, no pèrdues, així que convé poder tornar enrere.
Fase 6: llegir la dinàmica — el que és normal, el mode collapse i què tocar
Evolució visual típica i honesta (les teves èpoques exactes variaran):
| Èpoques | Què veuràs a la graella |
|---|---|
| 1-3 | Soroll gris amb taques: res de recognoscible |
| 5-10 | Taques clares sobre fons fosc: «proto-peces» borroses |
| 15-25 | Siluetes clares: es distingeixen samarretes, pantalons, sabates |
| 30-50 | Peces recognoscibles amb textura bàsica; vores encara toves |
Pèrdues: què és normal. A diferència d'un classificador, aquí les pèrdues no han de convergir a zero: és un equilibri, no un descens. Sa: G oscil·lant en ~0.7-1.5 i D en ~1.0-1.3, totes dues movent-se sense tendència clara. Senyals d'alarma:
| Símptoma | Diagnòstic | Què tocar (de 05-01) |
|---|---|---|
D → 0 i G creix sense parar |
Discriminador massa fort: el generador no rep senyal útil | Abaixa el lr del discriminador (p. ex. 1e-4), o afegeix soroll/etiquetes suavitzades (0.9 en lloc d'1) als reals |
| Les 16 mostres de la graella són gairebé idèntiques | Mode collapse: el generador ha trobat una imatge que enganya i la repeteix | Més entropia: apuja el lr del generador lleugerament, revisa el suavitzat d'etiquetes, reinicia des d'un checkpoint anterior |
| Tot oscil·la violentament i les mostres empitjoren | Learning rates massa alts | Abaixa tots dos lr a la meitat |
| Graelles estancades 15+ èpoques | Equilibri mort | Prova més capacitat al generador o més èpoques; de vegades només necessita temps |
El diagnòstic es fa amb les graelles primer i les corbes després — exactament al revés que en els projectes anteriors.
Fase 7: avaluació, desat del generador i ús posterior
Avaluació qualitativa. El protocol mínim: (1) graelles del soroll fix època a època — milloren?; (2) diversitat — genera 64 mostres noves i comprova que hi apareixen diversos tipus de peça; (3) un cop d'ull als veïns — per a alguna mostra generada, busca la imatge real més semblant del dataset i verifica que no és una còpia memoritzada.
Mètriques quantitatives, només el concepte. En recerca es fa servir el FID (Fréchet Inception Distance): compara estadístiques d'imatges reals i generades a l'espai de característiques d'una xarxa preentrenada — com més baix, millor. Per a aquest prototip n'hi ha prou de saber que existeix i que la inspecció visual disciplinada és l'estàndard pràctic a aquesta escala.
Lliurament. Només es desplega el generador — el discriminador ha estat l'entrenador personal i es queda a casa:
generador.save("models/generador_creativitats_v1.keras")
# Ús posterior: noves imatges des de soroll
gen = tf.keras.models.load_model("models/generador_creativitats_v1.keras")
noves = gen(tf.random.normal([8, DIM_SOROLL]), training=False)
noves = ((noves + 1) / 2).numpy() # a [0,1]: el "des-preprocessament" viatja documentat (06-05)Informe honest per a màrqueting: «dominem la mecànica generativa i podem produir imatges sintètiques de baixa resolució; per a creativitats en qualitat de producció calen models de difusió (08-03) i una anàlisi prèvia d'implicacions ètiques i de drets (08-01) — tota imatge sintètica publicada s'ha d'identificar com a tal».
Errors Comuns i Consells
- Entrenar el discriminador fins a la perfecció «primer»: un discriminador perfecte dona un gradient gairebé nul al generador. Han de créixer junts: un pas cadascun, com al nostre bucle.
- Canviar el soroll de la graella a cada època: sense soroll fix no pots saber si millora el generador o només ha canviat la mostra. El soroll fix és el teu conjunt congelat particular (06-05).
- Interpretar les pèrdues com en un classificador: que la pèrdua del generador pugi no vol dir que empitjori — potser el discriminador ha millorat. Graelles primer.
- Oblidar
drop_remainder=True: un últim lot de mida diferent pot trencar formes dins de@tf.function. - Desesperar-se a l'època 10: les GAN són lentes a arrencar i no lineals a millorar. Jutja cada 5-10 èpoques, amb checkpoints per tornar al millor punt.
Exercicis
- Afegeix suavitzat d'etiquetes al discriminador (reals = 0.9 en lloc d'1.0) i compara l'estabilitat de les corbes i les graelles amb la versió original durant 20 èpoques.
- Entrena amb
DIM_SOROLL = 2i visualitza què genera el model recorrent una malla de valors del soroll (per exemple, de -2 a 2 en cada dimensió). Què observes sobre l'espai latent? Hi apareix mode collapse? - Interpola a l'espai latent: agafa dos vectors de soroll
z1, z2, genera imatges per az = (1-t)*z1 + t*z2ambtde 0 a 1 en 8 passos, i mostra la transició. Relació amb els embeddings de 03-04.
Solucions
- Canvia a
perdua_discriminador:bce(tf.ones_like(logits_reals) * 0.9, logits_reals). Efecte típic: la pèrdua deDdeixa d'enfonsar-se cap a 0, la deGoscil·la menys i les graelles progressen de manera més estable — és un dels remeis de 05-01 en acció. - Amb només 2 dimensions latents, la malla
[(x, y) for x in np.linspace(-2,2,8) for y in np.linspace(-2,2,8)]genera una graella on regions veïnes produeixen peces semblants: visualitzes l'espai latent sencer. La diversitat total baixa (2 dimensions donen poc marge) i el risc de collapse augmenta: veuràs poques classes de peça representades. Conclusió: la dimensió del soroll acota quanta varietat pot codificar el generador. for t in np.linspace(0, 1, 8): imgs.append(gen((1-t)*z1 + t*z2, training=False)). La transició és suau: una samarreta es transforma gradualment en un pantaló passant per formes intermèdies plausibles. Com en els embeddings de 03-04, la proximitat a l'espai latent codifica similitud semàntica — el generador ha organitzat el soroll en un mapa continu de peces de roba.
Conclusió
Quart projecte lliurat i la promesa més antiga del curs saldada: el bucle adversari complet, escrit a mà amb dos GradientTape, dues pèrdues i dos optimitzadors, amb graelles de soroll fix com a instrument d'avaluació i un diagnòstic pràctic de la dinàmica GAN (equilibri, no convergència; mode collapse i els seus remeis). El resultat és un prototip honest — peces de roba recognoscibles, no fotografia de producte — i un informe clar de què caldria per a producció. Queda un projecte, i és el que tanca el cercle: a 07-05 tornarem al classificador d'imatges de 07-01 i batrem el seu 85 % amb transfer learning, comparant xifres cara a cara per prendre la decisió final de TecnoMarket.
Curs de Deep Learning
Mòdul 1: Introducció al Deep Learning
- Què és el Deep Learning?
- Història i evolució del Deep Learning
- Aplicacions del Deep Learning
- Conceptes bàsics de xarxes neuronals
- Preparació de l'entorn de treball
Mòdul 2: Fonaments de Xarxes Neuronals
- Perceptró i Perceptró Multicapa
- Funció d'activació
- Propagació cap endavant i cap enrere
- Optimització i funció de pèrdua
- La teva primera xarxa neuronal completa
Mòdul 3: Xarxes Neuronals Convolucionals (CNN)
- Introducció a les CNN
- Capes convolucionals i de pooling
- Arquitectures populars de CNN
- Aplicacions de CNN en reconeixement d'imatges
Mòdul 4: Xarxes Neuronals Recurrents (RNN)
- Introducció a les RNN
- LSTM i GRU
- Aplicacions de RNN en processament del llenguatge natural
- Seqüències i sèries temporals
Mòdul 5: Tècniques Avançades en Deep Learning
- Xarxes Generatives Adversàries (GAN)
- Autoencoders
- Transfer Learning
- Regularització i tècniques de millora
- Mecanismes d'atenció i Transformers
Mòdul 6: Eines i Frameworks
- Introducció a TensorFlow
- Introducció a PyTorch
- Comparació de frameworks
- Entorns de desenvolupament i recursos addicionals
- Desar, carregar i desplegar models
Mòdul 7: Projectes Pràctics
- Classificació d'imatges amb CNN
- Generació de text amb RNN
- Detecció d'anomalies amb Autoencoders
- Creació d'una GAN per a generació d'imatges
- Fine-tuning d'un model preentrenat
