En tancar el mòdul anterior vam dir que ja coneixes les tècniques —CNN, RNN, GAN, autoencoders, transfer learning, atenció— i que havia arribat el moment de consolidar les eines. Comencem per la que fas servir des de la lliçó 02-05 sense haver-la mirat mai de cara: TensorFlow. Cada cop que has escrit keras.Sequential, model.fit() o model.save(), a sota hi havia TensorFlow fent la feina pesada: representar les dades com a tensors, calcular gradients automàticament i executar les operacions en CPU o GPU. En aquesta lliçó obrim la caixa: entendràs què és realment TensorFlow, com funcionen els seus tensors, com calcula gradients amb GradientTape (reproduirem el descens del gradient manual de 02-03), quines tres maneres hi ha de construir models i com alimentar les teves xarxes amb pipelines de dades eficients. Per a l'equip de TecnoMarket, això marca la diferència entre "fer servir receptes" i "dominar l'eina".
Contingut
- Què és TensorFlow i el seu ecosistema
- Tensors: constants, variables, shapes i dtypes
- Broadcasting: operar amb formes diferents
- Diferenciació automàtica amb tf.GradientTape
- Les tres APIs per construir models
- Pipelines de dades eficients amb tf.data
- Callbacks útils durant l'entrenament
Què és TensorFlow i el seu ecosistema
TensorFlow és una plataforma de codi obert per a machine learning creada per Google i publicada el 2015. El nom mateix el descriu: fa fluir (flow) tensors (arrays multidimensionals) a través de grafs d'operacions matemàtiques.
Convé distingir les capes de l'ecosistema:
| Capa | Què és | Exemple d'ús |
|---|---|---|
| TensorFlow (nucli) | Motor de tensors, operacions i diferenciació automàtica | tf.matmul, tf.GradientTape |
| Keras | API d'alt nivell per definir i entrenar models | keras.Sequential, model.fit() |
| tf.data | Construcció de pipelines de dades | Dataset.from_tensor_slices(...) |
| TensorBoard | Visualització d'entrenaments | Corbes de pèrdua, histogrames |
| TFLite / TF.js / TF Serving | Desplegament en mòbil, navegador i servidors | Ho veurem a 06-05 |
El punt clau per a tu: Keras ÉS l'API oficial d'alt nivell de TensorFlow. No has estat fent servir "una altra cosa" durant el curs; has estat fent servir TensorFlow a través de la seva interfície més còmoda. La relació és com la d'un cotxe automàtic (Keras) i el seu motor (TensorFlow): fins ara conduïes; avui obrim el capó.
import tensorflow as tf
from tensorflow import keras
print(tf.__version__) # p. ex. 2.16.x
print(keras.__name__) # keras viu dins de l'ecosistema TF- Si vas executar
pip install tensorflowa 01-05, ja tens tot el que necessites per a aquesta lliçó. - TensorFlow detecta la GPU automàticament si n'hi ha una de disponible (a Colab: menú
Entorno de ejecución → Cambiar tipo → GPU, amb la interfície en castellà).
# Quins dispositius veu TensorFlow?
print(tf.config.list_physical_devices())
# [PhysicalDevice(name='/physical_device:CPU:0', ...), potser una GPU]Tensors: constants, variables, shapes i dtypes
Un tensor és un array multidimensional amb un tipus de dada (dtype) uniforme. Ja els coneixes conceptualment del curs: una imatge de producte de TecnoMarket és un tensor (alt, ample, 3), un lot de ressenyes vectoritzades és un tensor (batch, longitud).
tf.constant: tensors immutables
import tensorflow as tf
escalar = tf.constant(4.99) # preu d'un cable USB
vector = tf.constant([120.0, 15.5, 899.0]) # preus de 3 productes
matriu = tf.constant([[1, 2], [3, 4]]) # tensor 2D
print(escalar.shape) # () -> 0 dimensions
print(vector.shape) # (3,) -> 1 dimensió amb 3 elements
print(matriu.shape) # (2, 2)
print(matriu.dtype) # <dtype: 'int32'>
print(vector.dtype) # <dtype: 'float32'>Punts que has d'interioritzar:
- shape: la forma del tensor. Els errors de shape són, amb diferència, els més freqüents en deep learning; llegir bé un missatge
Incompatible shapes: (32, 10) vs (32, 1)t'estalviarà hores. - dtype: el tipus de dada. Per defecte els flotants són
float32(equilibri entre precisió i velocitat) i els entersint32. No es poden barrejar dtypes en una operació sense convertir abans ambtf.cast. - Un
tf.constantés immutable: no en pots canviar els valors un cop creat.
tf.Variable: tensors que aprenen
Els pesos d'una xarxa són variables: tensors el valor dels quals s'ha de poder actualitzar a cada pas d'entrenament. Quan a 02-03 parlàvem del "repartiment de la culpa" que ajusta cada pes, aquests pesos són internament objectes tf.Variable.
w = tf.Variable(3.0) # un pes inicialitzat a 3.0
b = tf.Variable(0.0) # un biaix
w.assign(2.5) # canviar-ne el valor (això un constant no ho pot fer!)
w.assign_add(0.1) # w = w + 0.1
print(w.numpy()) # 2.6 -> .numpy() converteix a numpytf.constant |
tf.Variable |
|
|---|---|---|
| Mutable | No | Sí (assign, assign_add) |
| Ús típic | Dades d'entrada, hiperparàmetres | Pesos i biaixos del model |
| Rastrejat per GradientTape | Només si es demana explícitament | Sí, automàticament |
Operacions bàsiques
a = tf.constant([[1.0, 2.0], [3.0, 4.0]])
b = tf.constant([[10.0, 20.0], [30.0, 40.0]])
print(a + b) # suma element a element
print(a * b) # producte element a element (NO matricial!)
print(tf.matmul(a, b)) # producte matricial (el de les capes denses)
print(tf.reduce_mean(a)) # mitjana de tots els elements -> 2.5
print(tf.reshape(a, (4, 1))) # canviar la forma sense canviar les dadesRecorda de 02-01: una capa densa és exactament tf.matmul(entrades, pesos) + biaix seguit d'una activació. Ara ja la pots escriure a mà.
Broadcasting: operar amb formes diferents
El broadcasting permet operar tensors de formes diferents: TensorFlow "estira" virtualment el tensor petit perquè encaixi amb el gran, sense copiar dades. És la mateixa regla que fa servir numpy.
preus = tf.constant([[120.0], [15.5], [899.0]]) # shape (3, 1): 3 productes
iva = tf.constant(1.21) # escalar, shape ()
amb_iva = preus * iva # l'escalar s'aplica als 3 -> shape (3, 1)
descomptes = tf.constant([0.95, 0.90, 0.80]) # shape (3,): 3 campanyes
taula = preus * descomptes # (3,1) x (3,) -> broadcasting -> (3, 3)
print(taula.shape) # (3, 3): cada producte amb cada descompteRegles pràctiques (les shapes es comparen de dreta a esquerra):
- Dues dimensions són compatibles si són iguals o si una de les dues és 1.
- Les dimensions que falten es tracten com a 1.
- Si cap regla no aplica, obtindràs un error
Incompatible shapes.
El broadcasting és potentíssim, però també és font d'errors silenciosos: a l'exemple anterior, potser volies 3 preus amb 3 descomptes (resultat (3,)) i has obtingut una taula (3, 3) sense cap error. Comprova sempre la shape del resultat.
Diferenciació automàtica amb tf.GradientTape
Aquí hi ha la joia de la corona. A 02-03 vam calcular gradients a mà amb numpy, aplicant la regla de la cadena pas a pas per repartir la culpa de l'error. TensorFlow fa exactament això, però automàticament: tf.GradientTape és una "cinta gravadora" que registra cada operació que fas dins del seu context, i després la reprodueix cap enrere per calcular derivades.
Reproduïm l'exemple clàssic: minimitzar una funció de pèrdua simple, primer el gradient i després el descens complet.
import tensorflow as tf
# Volem trobar el w que minimitza la "pèrdua" L(w) = (w - 4)^2
# Sabem per càlcul que el mínim és a w = 4 i que dL/dw = 2*(w - 4)
w = tf.Variable(0.0) # comencem lluny del mínim
with tf.GradientTape() as tape: # 1. la cinta comença a gravar
loss = (w - 4.0) ** 2 # 2. operacions gravades
grad = tape.gradient(loss, w) # 3. reproduir cap enrere
print(grad.numpy()) # -8.0 (= 2*(0-4), la derivada exacta)Explicació línia a línia:
with tf.GradientTape() as tape:obre el context de gravació. Tot el que passi a dins i que involucri variables queda registrat.loss = (w - 4.0) ** 2no només calcula el valor (16.0): la cinta anota "resta, després quadrat", que és el que necessita per aplicar la regla de la cadena.tape.gradient(loss, w)pregunta: "quant canvialosssi mocwuna miqueta?". És la mateixa pregunta del backpropagation de 02-03.
Ara el descens del gradient complet, el mateix bucle que vam escriure amb numpy:
w = tf.Variable(0.0)
learning_rate = 0.1
for pas in range(30):
with tf.GradientTape() as tape:
loss = (w - 4.0) ** 2
grad = tape.gradient(loss, w)
w.assign_sub(learning_rate * grad) # w = w - lr * gradient
print(w.numpy()) # ~3.995: ha convergit a 4, igual que a 02-03Comparació amb la versió numpy de 02-03:
| numpy (02-03) | TensorFlow (GradientTape) | |
|---|---|---|
| Derivada | L'escrius tu a mà: 2*(w-4) |
La calcula la cinta automàticament |
| Risc d'error | Alt (una derivada malament i tot falla) | Pràcticament nul |
| Xarxes profundes | Inviable a mà (milers de derivades) | Esforç idèntic: una línia |
| Valor didàctic | Entens el mecanisme | Escales el mecanisme |
Quan crides model.fit() a Keras, internament passa exactament això: un GradientTape grava el forward pass, calcula els gradients de la pèrdua respecte a totes les variables del model, i l'optimitzador (SGD, Adam...) les actualitza. fit() és aquest bucle, empaquetat.
Les tres APIs per construir models
TensorFlow/Keras ofereix tres maneres de definir un model. Dues ja les domines:
- Sequential (la coneixes des de 02-05)
Per a piles lineals de capes: entrada → capa → capa → sortida.
model = keras.Sequential([
keras.layers.Dense(128, activation="relu"),
keras.layers.Dense(64, activation="relu"),
keras.layers.Dense(10, activation="softmax"),
])
- API funcional (la vas fer servir a 03-03)
Per a grafs amb branques, múltiples entrades/sortides o connexions de salt, com el bloc residual que vam construir:
inputs = keras.Input(shape=(784,))
x = keras.layers.Dense(128, activation="relu")(inputs)
x = keras.layers.Dense(64, activation="relu")(x)
outputs = keras.layers.Dense(10, activation="softmax")(x)
model = keras.Model(inputs, outputs)
- Subclassing (la nova)
Defineixes una classe heretant de keras.Model i escrius tu mateix el forward pass a call(). Màxima flexibilitat: pots fer servir bucles, condicionals, lògica arbitrària.
class ClassificadorTecnoMarket(keras.Model):
def __init__(self):
super().__init__()
self.densa1 = keras.layers.Dense(128, activation="relu")
self.densa2 = keras.layers.Dense(64, activation="relu")
self.sortida = keras.layers.Dense(10, activation="softmax")
def call(self, x):
x = self.densa1(x) # tu decideixes el flux de dades
x = self.densa2(x)
return self.sortida(x)
model = ClassificadorTecnoMarket()- A
__init__declares les capes (les peces). - A
callconnectes les peces (el flux). S'executa a cada forward pass.
| API | Quan fer-la servir | Flexibilitat | Ja vista a |
|---|---|---|---|
| Sequential | Pila lineal simple | Baixa | 02-05 |
| Funcional | Grafs amb branques i salts | Mitjana-alta | 03-03 |
| Subclassing | Lògica arbitrària, recerca | Màxima | Aquesta lliçó |
Consell: fes servir sempre l'API més simple que resolgui el teu problema. El subclassing et resultarà familiar quan vegis PyTorch a la propera lliçó: allà és la manera estàndard de treballar.
Pipelines de dades eficients amb tf.data
Fins ara passàvem arrays numpy directament a fit(). Funciona amb MNIST, però el catàleg real de TecnoMarket té centenars de milers de fotos i ressenyes que no caben en memòria. tf.data construeix pipelines: canonades que carreguen, transformen i serveixen les dades per lots, en paral·lel amb l'entrenament.
import tensorflow as tf
# Simulem ressenyes de TecnoMarket ja vectoritzades (com a 04-03) i les seves etiquetes
import numpy as np
ressenyes = np.random.rand(10000, 200).astype("float32") # 10k ressenyes
etiquetes = np.random.randint(0, 2, size=(10000,)) # 0=negativa, 1=positiva
dataset = tf.data.Dataset.from_tensor_slices((ressenyes, etiquetes))
dataset = (dataset
.shuffle(buffer_size=10000) # 1. remenar (evita que el model memoritzi l'ordre)
.batch(32) # 2. agrupar en lots de 32
.prefetch(tf.data.AUTOTUNE)) # 3. preparar el lot següent mentre s'entrena
# Es fa servir directament a fit, sense batch_size (ja el porta el dataset):
# model.fit(dataset, epochs=5)Què fa cada pas i per què importa:
shuffle(buffer_size): manté un buffer d'elements i n'extreu a l'atzar. Si el CSV de ressenyes està ordenat per producte, sense shuffle cada lot seria monotemàtic i l'entrenament oscil·laria. Idealmentbuffer_size≥ mida del dataset (si hi cap).batch(32): el descens del gradient per mini-batches de 02-04 necessita lots; aquí es formen.prefetch(AUTOTUNE): mentre la GPU entrena amb el lot N, la CPU prepara el lot N+1. Sense prefetch, la GPU espera aturada que arribin les dades; amb prefetch, la canonada no es buida mai.AUTOTUNEdeixa que TensorFlow triï quants lots anticipar.
Per a les fotos de producte, el patró habitual afegeix map per descodificar i transformar en paral·lel:
def carregar_foto(ruta, etiqueta):
img = tf.io.read_file(ruta) # llegir bytes del disc
img = tf.image.decode_jpeg(img, channels=3) # bytes -> tensor (h, w, 3)
img = tf.image.resize(img, [224, 224]) / 255.0 # mida fixa i normalitzar
return img, etiqueta
ds_fotos = (tf.data.Dataset.from_tensor_slices((rutes, etiquetes_fotos))
.map(carregar_foto, num_parallel_calls=tf.data.AUTOTUNE) # en paral·lel
.shuffle(1000)
.batch(32)
.prefetch(tf.data.AUTOTUNE))Així és com l'equip de TecnoMarket alimentaria la MobileNetV2 de 05-03 amb el seu catàleg complet sense esgotar la memòria: les imatges es llegeixen del disc just quan es necessiten.
Ordre recomanat: map (transformar) → shuffle (remenar) → batch (agrupar) → prefetch (anticipar). Remenar abans d'agrupar garanteix lots diferents a cada època.
Callbacks útils durant l'entrenament
Els callbacks són objectes que Keras invoca en moments clau de l'entrenament (final de cada època, de cada lot...). Ja vas fer servir EarlyStopping a 05-04; completem el kit bàsic:
callbacks = [
# Ja el coneixes de 05-04: aturar quan la validació deixa de millorar
keras.callbacks.EarlyStopping(patience=3, restore_best_weights=True),
# NOU: desar automàticament el millor model vist fins al moment
keras.callbacks.ModelCheckpoint(
"tecnomarket-dl/models/millor_model.keras",
monitor="val_loss", # quina mètrica vigilar
save_best_only=True), # només sobreescriure si millora
# NOU: registrar mètriques per visualitzar-les amb TensorBoard
keras.callbacks.TensorBoard(log_dir="tecnomarket-dl/logs"),
]
# model.fit(dataset, validation_data=ds_val, epochs=50, callbacks=callbacks)ModelCheckpointambsave_best_only=Trueés la teva xarxa de seguretat: encara que l'entrenament es talli a mitges (Colab desconnecta, se'n va la llum), el millor model és fora de perill al disc. Compara-ho amb elmodel.save()manual de 02-05: aquell desava al final; aquest desa el millor, sobre la marxa.TensorBoardescriu logs que després es visualitzen com a corbes interactives; veurem com fer-lo servir a la pràctica a 06-04.- Els tres callbacks conviuen sense problema a la mateixa llista: vigilen, desen i registren alhora.
Errors Comuns i Consells
- Barrejar dtypes:
tf.constant(1) + tf.constant(1.0)falla (int32+float32). Solució:tf.cast(x, tf.float32). Consell: escriu els teus números amb decimal (1.0) des del principi. - Oblidar que les constants no es rastregen: si defineixes un pes com a
tf.constant,tape.gradientretornaràNone. Els paràmetres entrenables han de sertf.Variable. - Fer servir la cinta dues vegades: per defecte
GradientTapees consumeix en cridargradient(). Si necessites diversos gradients de la mateixa gravació, crea la cinta ambpersistent=True(i esborra-la després ambdel tape). - Broadcasting silenciós: una operació entre
(n, 1)i(n,)produeix(n, n)sense avisar. Imprimeixresultat.shapequan dubtis; és gratis i evita hores de depuració. shuffleamb un buffer minúscul:shuffle(10)sobre 100.000 ressenyes gairebé no remena res. Fes servir un buffer de la mida del dataset o, si no hi cap, tan gran com sigui possible.- Consell:
tensor.numpy()converteix qualsevol tensor a numpy per inspeccionar-lo o graficar-lo. És el teu pont de tornada al món conegut.
Exercicis
Exercici 1: tensors i broadcasting
TecnoMarket té 4 productes amb preus [120.0, 15.5, 899.0, 45.0] i vol aplicar tres escenaris de descompte: 5 %, 15 % i 30 %. Fent servir broadcasting (sense bucles), construeix una taula de shape (4, 3) on cada fila sigui un producte i cada columna un escenari amb el preu final. Comprova la shape del resultat.
Exercici 2: gradient amb GradientTape
Fes servir tf.GradientTape per calcular el gradient de L(w, b) = (3*w + b - 10)**2 respecte a w i b, partint de w=1.0, b=0.0. Després escriu un bucle de descens del gradient (learning rate 0.01, 200 passos) i comprova que la pèrdua final és gairebé zero. Hi ha una única solució (w, b) possible?
Exercici 3: pipeline tf.data
Crea un tf.data.Dataset a partir de 1.000 mostres sintètiques (np.random.rand(1000, 20) com a característiques i etiquetes binàries aleatòries) amb shuffle, lots de 64 i prefetch. Itera-hi i imprimeix la shape dels dos primers lots. Per què el darrer lot d'una època pot tenir menys de 64 elements i com ho evitaries?
Solucions
Solució 1:
import tensorflow as tf
preus = tf.constant([[120.0], [15.5], [899.0], [45.0]]) # shape (4, 1)
factors = tf.constant([0.95, 0.85, 0.70]) # shape (3,)
taula = preus * factors # broadcasting: (4,1) x (3,) -> (4,3)
print(taula.shape) # (4, 3)
print(taula.numpy().round(2))La clau és donar als preus shape (4, 1) (columna): així cada dimensió de mida 1 s'"estira" contra l'altra.
Solució 2:
w = tf.Variable(1.0)
b = tf.Variable(0.0)
with tf.GradientTape() as tape:
loss = (3.0 * w + b - 10.0) ** 2
grads = tape.gradient(loss, [w, b])
print([g.numpy() for g in grads]) # [-42.0, -14.0]
# dL/dw = 2*(3w+b-10)*3 = 2*(-7)*3 = -42 ; dL/db = 2*(-7)*1 = -14
lr = 0.01
for _ in range(200):
with tf.GradientTape() as tape:
loss = (3.0 * w + b - 10.0) ** 2
gw, gb = tape.gradient(loss, [w, b])
w.assign_sub(lr * gw)
b.assign_sub(lr * gb)
print(loss.numpy()) # ~0.0
print(w.numpy(), b.numpy())No hi ha solució única: qualsevol parell amb 3w + b = 10 anul·la la pèrdua (una recta sencera de solucions). El descens del gradient en troba una, la més propera al punt de partida. Això anticipa per què en xarxes reals inicialitzacions diferents donen pesos finals diferents amb rendiment similar.
Solució 3:
import numpy as np, tensorflow as tf
X = np.random.rand(1000, 20).astype("float32")
y = np.random.randint(0, 2, size=(1000,))
ds = (tf.data.Dataset.from_tensor_slices((X, y))
.shuffle(1000)
.batch(64)
.prefetch(tf.data.AUTOTUNE))
for i, (xb, yb) in enumerate(ds.take(2)):
print(f"Lot {i}: X {xb.shape}, y {yb.shape}") # (64, 20) i (64,)1000 / 64 = 15 lots complets i un residu de 40: el darrer lot té 40 elements. Si el teu codi exigeix lots de mida fixa, fes servir batch(64, drop_remainder=True) (a canvi de descartar aquestes 40 mostres a cada època).
Conclusió
Has obert el capó de l'eina que feies servir des de l'inici del curs: TensorFlow representa les dades com a tensors (constants per a dades, variables per a pesos), calcula gradients amb GradientTape —el mateix backpropagation de 02-03, automatitzat—, ofereix tres APIs de construcció (Sequential, funcional i subclassing, de menys a més flexible), alimenta els models amb pipelines tf.data (shuffle → batch → prefetch) i vigila l'entrenament amb callbacks com ModelCheckpoint. Amb això, model.fit() ha deixat de ser màgia: és un bucle de GradientTape amb extres.
A la lliçó següent coneixeràs l'altre gegant: PyTorch, el framework on aquest bucle d'entrenament no ve empaquetat sinó que l'escrius tu, línia a línia. Veuràs que tot el que has après avui —tensors, autograd, subclassing— hi té el seu bessó directe, i reconstruirem la xarxa MNIST de 02-05 per comprovar-ho.
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
