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

  1. Què és TensorFlow i el seu ecosistema
  2. Tensors: constants, variables, shapes i dtypes
  3. Broadcasting: operar amb formes diferents
  4. Diferenciació automàtica amb tf.GradientTape
  5. Les tres APIs per construir models
  6. Pipelines de dades eficients amb tf.data
  7. 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 tensorflow a 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 enters int32. No es poden barrejar dtypes en una operació sense convertir abans amb tf.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 numpy
tf.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 dades

Recorda 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 descompte

Regles pràctiques (les shapes es comparen de dreta a esquerra):

  1. Dues dimensions són compatibles si són iguals o si una de les dues és 1.
  2. Les dimensions que falten es tracten com a 1.
  3. 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) ** 2 no 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 canvia loss si moc w una 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-03

Comparació 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:

  1. 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"),
])

  1. 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)

  1. 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 call connectes 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. Idealment buffer_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. AUTOTUNE deixa 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)
  • ModelCheckpoint amb save_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 el model.save() manual de 02-05: aquell desava al final; aquest desa el millor, sobre la marxa.
  • TensorBoard escriu 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.gradient retornarà None. Els paràmetres entrenables han de ser tf.Variable.
  • Fer servir la cinta dues vegades: per defecte GradientTape es consumeix en cridar gradient(). Si necessites diversos gradients de la mateixa gravació, crea la cinta amb persistent=True (i esborra-la després amb del tape).
  • Broadcasting silenciós: una operació entre (n, 1) i (n,) produeix (n, n) sense avisar. Imprimeix resultat.shape quan dubtis; és gratis i evita hores de depuració.
  • shuffle amb 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

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