Introducció
Aquests són els continguts d’aquest portal web:
- Serveis i processos: mòdul M9 de DAM
- Interfícies: mòdul M7 de DAM
- IA i Ciència de dades: mòdul optatiu de DAM
- Fonaments de programació: conceptes generals de programació
- Programació en Java: àmbits de programació amb Java
- Web: programació en l’entorn web
- Persistència: conceptes bàsics de persistència
- Empresa: continguts d’emprenedoria
Serveis i processos
Processos i fils
Referències
- Java Concurrency Terminology
- Java Language Specification, capítol 17: Threads and Locks
- The Java tutorials: Concurrency
- Java Concurrency and Multithreading Tutorial
- Threads (Java in a nutshell)
- How to work with wait(), notify() and notifyAll() in Java?
- Thread Communication using wait/notify
- The evolution of the producer / consumer problem in Java
- Java CompletableFuture Tutorial with Examples
- Concurrency in JavaFX 8
- Liveness (The Java Tutorials, Oracle)
- Liveness (Wikipedia)
- The Deadlock Empire
- Thread Safety (6.005: Software Construction)
- What is Thread Dump and How to Analyze them?
- Concurrency: A Primer
Programació i sistemes reactius:
- Reactive Programming: Why It Matters
- Reactive programming vs Reactive systems
- RxJava Wiki
- RxJava Backpressure
- Reactive Manifesto (Glossary)
- Poor Man’s Actors in Java
- Reactive in practice: A complete guide to event-driven systems development in Java
- Reactive Programming with Reactor 3
Concurrència
Concurrència i paral·lelisme
La computació concurrent permet que diverses tasques dins d’una aplicació puguin executar-se sense un ordre concret, fora d’una seqüència. O sigui: no cal que una tasca es completi perquè comenci la següent. Aquesta és una propietat de l’algorisme. És a dir, cal que dissenyem la nostra aplicació perquè ho permeti.
La computació concurrent no implica que l’execució es produeix en el mateix instant de temps. La computació paral·lela sí que fa referència a l’execució en el mateix instant de temps, i treu profit de sistemes amb múltiples cores per a accelerar la computació. És una propietat de la màquina.
Per una banda, hi ha situacions on les aplicacions són inherentment concurrents. Per una altra, si no dissenyem concurrentment, les nostres aplicacions no poden aprofitar les arquitectures hardware multi-core de les CPU dels ordinadors, i estarem limitats a la capacitat i rendiment d’un sol core.
Processos i fils
El planificador o scheduler d’un sistema és el mecanisme que permet assignar tasques a treballadors. A una aplicació, una tasca sol traduir-se en un fil o thread i un treballador en un core de CPU. L’assignació fa que el planificador substitueixi una tasca per un altra. D’això se’n diu canvi de context (context switching). És un procés pesat per als processos, amb un context més gran, i lleuger per als fils, amb un context més petit.
La unitat bàsica d’execució d’un sistema operatiu és el procés, que són col·leccions de codi, memòria, dades i altres recursos. Un procés té un entorn independent d’execució, simulant un ordinador. Per exemple, té el seu espai independent de memòria. Solen ser sinònim de programa, o aplicació, encara que pugui ser un conjunt de processos.
Un fil és una seqüència de codi que s’executa dins de l’àmbit del procés, i que pot compartir dades amb altres fils. Un fil és la seqüència mínima d’instruccions que gestiona un scheduler. Un procés pot tenir diversos fils executant-se simultàniament a dins.
Quan desenvolupem una aplicació, les seves dades es troben en dos espais de memòria diferents: el heap i la pila de crides (call stack):
-
Pila de crides (call stack): estructura de dades que guarda les rutines actives d’un fil apilades en frames. El frame es crea quan es fa una crida, i s’esborra quan s’acaba la rutina. Cada frame conté:
- L’adreça de retorn
- Els paràmetres de la rutina
- Les variables locals
-
Heap: espai dinàmic de memòria que s’assignen quan es creen dades i es desassignen quan s’esborren. Habitualment aquí trobem els objectes.
El processos són totalment independents, no comparteixen res a la pila o el heap. Els fils poden compartir dades del heap.
Aplicacions de la concurrència
La programació ens permet implementar concurrència de diverses formes segons el llenguatge i el context del desenvolupament. Aquests són possibles casos d’ús:
- A una UI, fer operacions en un treballador independent que no bloquegi la interfície.
- Implementar alarmes i temporitzadors.
- Implementació d’algorismes paral·lels.
- Implementar tasques de múltiples clients concurrents, accedint a recursos compartits.
Models de concurrència
Una tasca pot caracteritzar-se, segons el seu tipus d’activitat, com:
- Limitada per la CPU: és una tasca que necessita la CPU per fer càlculs intensius.
- Limitada per l’E/S (Entrada/Sortida): és una tasca que habitualment està esperant per una operació d’entrada/sortida, com pot ser llegir o escriure a disc o a la xarxa.
L’assignació de tasques del scheduler pot ser de dos tipus: cooperativa o apropiativa.
- Cooperativa: les tasques gestionen el seu cicle de vida, i decideixen quan abandonen el treballador.
- Apropiativa: el scheduler assigna un time slice per a la tasca, i la treu del treballador quan es compleix.
El principal repte per implementar la concurrència és la correcta coordinació entre tasques i l’accés als recursos compartits de manera segura. Aquests són alguns dels enfocaments disponibles:
- Un sol fil: només tenim un fil que es comparteix entre les tasques.
- Estat compartit (o memòria compartida): dues o més tasques comparteixen un estat que totes poden llegir i escriure, i diversos mecanismes permeten fer-ho de manera segura.
- Pas de missatges: no es comparteix res. En el seu lloc, s’intercanvien missatges.
El següent diagrama mostra el model d’un sol fil, el de memòria compartida i el de pas de missatges.

Un sol fil
Aquesta opció simplifica el disseny de la concurrència, ja que no cal utilitzar mecanismes per a gestionar l’accés simultani a recursos compartits. Té el desavantatge que no es poden paral·lelitzar les tasques, però això només és un problema si estan limitades per CPU.
Un exemple habitual és el del bucle d’esdeveniments de les UI. S’implementa amb una cua que rep els esdeveniments i els gestiona ràpidament, ja que només realitzen operacions asíncrones.
Estat compartit
Les tasques concurrents interaccionen llegint i escrivint objectes compartits i mutables en memòria. És complex, ja que cal implementar mecanismes de bloqueig per coordinar els fils.
Imaginem que els fils A i B utilitzen un mateix codi per a compartir els objectes mutables. Aquest codi, que permet que diversos fils l’accedeixin simultàniament de forma segura, s’anomena “thread-safe”.
Hi ha quatre estratègies que veurem: confinament, immutabilitat, tipus thread-safe i sincronització.
Pas de missatges
Les tasques concurrents interaccionen enviant-se missatges entre ells (1:1 o N:1) a través d’un canal de comunicació. Les tasques envien missatges amb objectes immutables, i els missatges entrants de cada tasca es col·loquen en cola per a la seva gestió. Ho poden fer de forma síncrona o asíncrona, en funció de si s’espera o no la resposta.
El pass de missatges es pot implementar en dos contextos: entre fils d’un procés (p.ex. cues i productor/consumidor) o entre processos d’una xarxa (p. ex. amb sòcols).
Estat compartit
El disseny de programari concurrent que comparteix dades es basa en la idea que els fils poden accedir a les mateixes dades, i que aquestes dades poden ser modificades per qualsevol fil.
Un exemple de llenguatge que utilitza aquest model és Java.
Tenim dos reptes a resoldre:
-
L’accés segur a les dades compartides. Quins mecanismes podem utilitzar per assegurar-nos que els fils accedeixen de forma segura a les dades compartides?
-
La coordinació entre els fils. Com podem gestionar l’ordre d’execució dels fils, sincronitzar-los quan calgui i gestionar les seves interaccions?
Accés segur
Si l’accés de tots els fils és només de lectura, no hi ha problema. El problema és quan hi ha accés simultani de lectura i escriptura.
HI ha dues situacions problemàtiques: la interferència de fils i la coherència de dades.
Interferència de fils
La interferència es produeix quan dues operacions, que s’executen en diferents fils, però que actuen sobre les mateixes dades, s’entrellacen. Això vol dir que les dues operacions consten de diversos passos i les seqüències de passos se superposen. Aquest fenomen també s’anomena race condition.
Per exemple: l’operació d’increment d’un comptador consta de llegir el valor actual, incrementar-lo i escriure el nou valor. Si dues operacions d’increment s’executen simultàniament, el resultat final pot ser erroni.

La solució és sincronitzar l’accés a les dades compartides, fent que cada operació s’executi de forma atòmica.

Coherència de dades
El problema és que dues operacions, que s’executen en diferents fils, però que actuen sobre les mateixes dades, no veuen els canvis que s’han fet en les dades. Això es deu a que els fils tenen una còpia local de les dades compartides, i no es veuen els canvis que fan els altres fils.
Per exemple, si un fil canvia una variable booleana de valor i un altre fil llegeix el valor, pot ser que no vegi el canvi. Això es deu a que el compilador pot optimitzar el codi, i no llegir el valor de la variable cada vegada que es fa servir, sinó que el guarda en un registre. Això fa que el fil no vegi els canvis que fan els altres fils.
Estratègies
Les possibles estratègies per a gestionar l’accés simultani a dades compartides són: confinament, immutabilitat, tipus thread-safe i sincronització.
- Confinament. No compartiu la variable entre fils.
- Immutabilitat. Feu que les dades compartides siguin immutables. Tots els camps de la classe han de ser finals.
- Tipus de dades thread-safe. Encapsulem les dades compartides en un tipus de dades existent amb seguretat que realitzi la coordinació.
- Sincronització. Utilitzeu la sincronització per evitar que els fils accedeixin al mateix temps establint zones crítiques de codi: fragments de codi que accedeixen a dades compartides i que s’han d’executar de forma atòmica.
Coordinació
A l’hora de dissenyar el flux del programari concurrent, fes-te aquestes preguntes:
- Cal entendre la solució al problema. Habitualment, parteix de la solució seqüencial, per trobar la concurrent.
- Considera si pot ser paral·lelitzada. Alguns problemes són inherentment seqüencials.
- Pensa en les oportunitats de paral·lelitzar que permeten les dependències entre les dades. Si no hi ha dependències, podem descompondre-les i paral·lelitzar-les.
- Busca els llocs on la solució consumeix més recursos, com a candidats de paral·lelització.
- Descompon en tasques el problema, per veure si aquestes poden ser executades independentment.
Aquests són alguns mecanismes disponibles a diferents llenguatges per a coordinar els fils:
- Crear una o més tasques que aniran executant-se en paral·lel.
- Que un fil esperi que es completi un altre (join).
- Que un fil notifiqui a un altre que ha completat una tasca (notify).
- Que un fil esperi que un altre li notifiqui que ha completat una tasca (wait).
- Que un fil envii un senyal d’interrupció a un altre fil (interrupt).
Vitalitat (liveness) d’un sistema multifil
La vitalitat d’una aplicació (liveness) és la seva capacitat per a executar-se en el temps que toca. Els problemes més habituals que poden desbaratar aquesta vitalitat són:
- El deadlock (interbloqueig): dos o més fils es bloquegen per sempre, esperant l’un per l’altre. Pot passar si dos fils bloquegen recursos que necessiten esperant que estiguin lliures d’altres, que mai ho seran.
- La starvation (inanició): la denegació perpètua dels recursos necessaris per a processar una feina. Un exemple seria l’ús de prioritats, on sempre els fils amb més prioritat són atesos, i els altres mai ho són.
- El livelock és molt semblant al deadlock, però els fils sí que canvien el seu estat, tot i que mai s’arriba a una situació de desbloqueig.
Quan un client fa una petició a un servidor, el servidor ha d’aconseguir l’accés exclusiu als recursos compartits necessaris. L’ús correcte de les zones crítiques permetrà que el sistema tingui una millor vitalitat quan la càrrega de peticions sigui alta.
Java
Java és un llenguatge de programació d’alt nivell orientat a objectes, compilat i interpretat. Els seus programes són compilats a bytecode, que és interpretat per la màquina virtual de Java (JVM). Aquesta màquina virtual és un procés que s’encarrega de gestionar la memòria i els seus fils d’execució.

El call stack de Java pot contenir dades primitives i referències a objectes. Els objectes es guarden al heap, i el Garbage Collector s’encarrega de gestionar la seva memòria.
El llenguatge Java implementa la concurrència fent ús del model d’estat compartit i fils.
Una aplicació Java pot crear processos addicionals utilitzant ProcessBuilder. Aquesta classe permet crear processos de sistema (Process) i executar-los, parar-los, llegir la seva sortida, reencaminar-la, etc. Resumint, permet interactuar amb altres processos de sistema que no siguin de la màquina virtual Java.
Tanmateix, Java basa la seva implementació de concurrència en la utilització de fils d’execució, que serà el que veurem a continuació. Aquesta es pot implementar a dos nivells: l’API de baix nivell basada en fils i els objectes concurrents d’alt nivell.
Fils a Java
L’API de baix nivell de Java es basa en l’objecte Thread, que pot tenir una sèrie d’estats.
- NEW: Un fil que no s’ha iniciat.
- RUNNABLE: Un fil que s’està executant a la JVM. És possible que no tingui la CPU.
- BLOCKED: Un fil que està esperant per entrar a un bloc sincronitzat (esperant un monitor lock).
- WAITING: Un fil que espera indefinidament per un altre (operacions wait, join).
- TIMED_WAITING: Un fil que espera un cert temps per un altre (operacions sleep, wait, join).
- TERMINATED: Un fil que ha acabat. No es pot tornar a executar.
Podem crear un fil de dues maneres:
- Estendre la classe Thread i reescriure el mètode “run” (millor no utilitzar aquest mètode).
- Implementar la interfície Runnable i el seu mètode “run”. Llavors, crear un Thread passant aquest objecte al constructor:
new Thread(new MyRunnable())
Un cop tenim el Thread, el podem executar mitjançant el seu mètode start(), que farà canviar el seu estat de NEW a RUNNABLE.
Per als exemples que veurem a continuació, utilitzarem la següent utility class:
public class Threads {
private static long start = System.currentTimeMillis();
private Threads() {}
public static void log(String message) {
System.out.println(String.format("%6d %-10s %s",
System.currentTimeMillis() - start,
Thread.currentThread().getName(), message));
}
public static void spend(long millis) {
long startTime = System.currentTimeMillis();
while (System.currentTimeMillis() - startTime < millis);
}
public static void rest(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
Els seus mètodes poden cridar-se després de fer un import static Threads.*;.
A continuació, podem veure un exemple de creació d’un fil anomenat “fil” des del fil principal, “main”. Mostra l’estat del fil “child” abans i després de la seva execució.

Aquest seria el codi:
public class StatesThread {
static class MyRunnable implements Runnable {
@Override
public void run() {
log("spending");
spend(750);
log("resting");
rest(750);
log("ending");
}
}
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable(), "child");
log(thread.getState().name());
thread.start();
rest(250);
log(thread.getState().name());
rest(875);
log(thread.getState().name());
rest(500);
log(thread.getState().name());
}
}
Aquesta podria ser una sortida per pantalla possible:
0 main NEW
8 child spending
258 main RUNNABLE
758 child resting
1133 main TIMED_WAITING
1508 child ending
1633 main TERMINATED
Aquest exemple no presenta dificultats, ja que no es comparteix cap dada. Tampoc s’utilitza cap mecanisme de sincronització entre els dos fils.
Tècniques de disseny
Hi ha bàsicament quatre tècniques per assegurar-nos que no tindrem problemes accedint a variables en memòria compartida. Són aquestes, per ordre de preferència:
- Confinament. No compartiu objectes entre fils.
- Immutabilitat. Feu que les dades compartides siguin immutables. Tots els camps de la classe han de ser finals.
- Utilitzar tipus de dades thread-safe. Encapsulem les dades compartides en un tipus de dades existent amb seguretat que realitzi la coordinació.
- Per exemple, el paquet java.util.concurrent també conté algunes classes concurrents de mapes, cues, conjunts, llistes i variables atòmiques. Aquestes classes es poden utilitzar i compartir sense por a provocar race conditions.
- Sincronització. Utilitzeu la sincronització per evitar que els fils accedeixin les dades al mateix temps.
- Els objectes monitor són objectes a què només pot accedir un fil alhora. Aquests permeten definir zones crítiques de codi. És el mètode més utilitzat.
- També es pot utilitzar els reentrant locks (lock / unlock).
A continuació veurem un exemple de race condition, el problema més representatiu de l’estat compartit entre fils. Dos fils intenten incrementar un comptador a un objecte compartit. El resultat final no és el que esperem:
public class RaceThread {
static class SharedObject {
int counter;
}
static class MyRunnable implements Runnable {
SharedObject so;
MyRunnable(SharedObject so) {
this.so = so;
}
void increment() {
so.counter ++;
}
@Override
public void run() {
for (int i=0; i<1_000_000; i++) {
increment();
}
}
}
public static void main(String[] args) throws InterruptedException {
SharedObject so = new SharedObject();
Thread t1 = new Thread(new MyRunnable(so));
Thread t2 = new Thread(new MyRunnable(so));
t1.start();
t2.start();
t1.join();
t2.join();
log("counter is " + so.counter);
}
}
En aquest exemple, el mètode increment() no és thread-safe. El resultat és que el valor de counter no és el que esperàvem. Això és degut a que el mètode increment() no és atòmic. Això vol dir que no és una única operació, sinó que es descompon en diverses operacions més petites. És el que es diu una operació composta. El problema és que cada petita operació pot intercalar-se entre diversos fils i generar un problema.
En el nostre cas, el compilador descompon el mètode increment() en tres operacions anomenades Read-Modify-Write:
- Llegir el valor de
counterde memòria. - Incrementar el valor llegit.
- Escriure el valor incrementat a memòria.
Aquestes són algunes operacions compostes habituals:
- Read-Modify-Write (l’exemple)
- Check-Then-Act: per exemple, inicialitzar si cal (singleton)
- Put-If-Absent: afegir un element a una col·lecció si no existeix
Per a sincronitzar, necessitem definit el concepte de monitor (lock): un monitor és un objecte que només pot ser accedit per un únic fil al mateix temps. En el nostre problema, el monitor seria l’objecte so. Cada fil ha d’adquirir el monitor abans d’accedir a la variable counter. Això es fa amb la paraula clau synchronized.
Una primera solució seria no permetre l’accés directe al camp counter de l’objecte so. En aquest cas, el camp counter seria private i només es podria accedir a ell a través de mètodes. Aquests mètodes serien sincronitzats:
static class SharedObject {
private int counter;
synchronized void increment() {
counter ++;
}
synchronized int getCounter() {
return counter;
}
}
I canviar el mètode increment
Una segona solució seria fer que dos fils no poguessin accedir a la variable counter al mateix temps. Això a Java es fa utilitzant un monitor (lock). El mètode increment() quedaria així:
void increment() {
synchronized (so) {
so.counter ++;
}
}
Quan creem zones crítiques de codi amb els mecanismes de Java podem ordenar els esdeveniments que es produeixen. Les regles d’ordenació s’expliquen amb el concepte de “happen-before” (passa-abans). Resumint, es tracta de que si un esdeveniment A passa abans que un esdeveniment B, aleshores B no pot passar abans que A. Això ens permet assegurar-nos que els esdeveniments es produeixen en l’ordre que volem.
Hi ha una dificultat important a l’hora de dissenyar codi thread-safe, és a dir, que sigui segur davant l’accés de múltiples fils. Es poden preparar proves per al nostre codi que comprovin si, un nombre important de fils executant simultàniament el nostre codi, provoca problemes. Però no sempre és fàcil simular aquesta situació.
Si mirem la documentació de la Java Standard Edition, veurem que de vegades es fa referència a la condició “thread-safe” de les classes.
Per exemple, a la classe java.util.regex.Pattern es diu:
- Instances of this class are immutable and are safe for use by multiple concurrent threads. Instances of the
Matcherclass are not safe for such use.
És important que quan dissenyem el nostre codi siguem conscients de si necessitem que més d’un fil accedeixi. I si és així, dissenyar la classe en conseqüència i documentar-ho.
Join, interrupt i volatile
Join
Un join fa que un fil s’esperi a la finalització d’un altre. El fil que fa el join passa a l’estat WAITING fins que el fil que s’espera acaba. Si el fil que s’espera ja ha acabat, el join no fa res.

public class JoinThread {
static class MyRunnable implements Runnable {
@Override
public void run() {
log("starting");
spend(1500);
log("ending");
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new MyRunnable(), "child");
log("starting");
thread.start();
log("started");
thread.join();
log("joined");
}
}
Interrupt
Una interrupció és una indicació a un fil de què ha de parar de fer el que està fent i fer una altra cosa. Els fils tenen un flag que indica si han estat interromputs o no. Hi ha dues formes de comprovar si un fil ha estat interromput:
- Que el fil faci crides freqüents a mètodes que facin throw de InterruptedException. Per exemple, Thread.sleep(). També serveix si la interrupció s’ha produït abans del sleep().
- Que el fil comprovi freqüentment Thread.currentThread().isInterrupted().
Un cop feta la comprovació i detectada la interrupció, el flag d’interrupció del fil es retorna a false i cal decidir què fer. Normalment, el que farà és acabar la seva execució, però no és obligatori. Si no es fa res, el fil continuarà executant-se.

public class Interrupt1Thread {
static class MyRunnable implements Runnable {
@Override
public void run() {
log("starting");
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
log("interrupted!");
}
log("ending");
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new MyRunnable(), "child");
log("starting");
thread.start();
log("started");
thread.interrupt();
thread.join();
log("joined");
}
}
public class Interrupt2Thread {
static class MyRunnable implements Runnable {
@Override
public void run() {
log("starting");
while (!Thread.currentThread().isInterrupted());
log("ending");
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new MyRunnable(), "child");
log("starting");
thread.start();
log("started");
rest(1500);
thread.interrupt();
thread.join();
log("joined");
}
}
Estat compartit
Es pot acabar un fil compartint una variable que un fil modifica i l’altre llegeix. En Java, cal definir la variable com a volatile, indicant que els canvis fets en un fil siguin visibles en la resta. O bé utilitzar objectes segurs creats per nosaltres (mecanismes de sincronització) o d’una llibreria segura (p. ex. la atomic de Java).
La paraula clau volatile només s’ha d’utilitzar si un fil escriu i l’altre (o altres) llegeixen. Si diversos fils escriuen i llegeixen, cal gestionar-ho com a zones crítiques.

public class VolatileThread {
static class SharedObject {
boolean done; // volatile keyword needed
}
static class MyRunnable1 implements Runnable {
SharedObject so;
MyRunnable1(SharedObject so) {
this.so = so;
}
@Override
public void run() {
spend(1500);
so.done = true;
}
}
static class MyRunnable2 implements Runnable {
SharedObject so;
MyRunnable2(SharedObject so) {
this.so = so;
}
@Override
public void run() {
boolean done = false;
while (!done) {
done = so.done;
}
}
}
public static void main(String[] args) throws InterruptedException {
SharedObject so = new SharedObject();
Thread t1 = new Thread(new MyRunnable1(so));
Thread t2 = new Thread(new MyRunnable2(so));
t1.start();
t2.start();
t1.join();
log("joined 1 with " + so.done);
t2.join();
log("joined 2 with " + so.done);
}
}
Mecanismes de sincronització
Monitors (intrinsic lock)
A Java, la sincronització es fa mitjançant monitors. Un monitor és un objecte qualsevol que pot tenir un únic fil propietari. Qualsevol fil pot demanar la propietat d’un monitor i a canvi accedir a una zona crítica de codi restringida. Si ja hi ha propietari, cal que s’esperi fins que ho deixi de ser.

Per demanar la propietat d’un monitor i l’accés a la zona crítica de codi, podem utilitzar la paraula reservada “synchronized”. En funció d’on ho fem, l’objecte monitor canvia:
- Mètodes d’instància: L’objecte monitor és la instància. Per tant, només un fil per cada instància.
- Mètodes de classe: L’objecte monitor és la classe. Per tant, només un fil per cada classe.
- Blocs de codi: s’ha d’indicar l’objecte monitor dins dels parèntesis. Qualsevol objecte pot ser monitor (p. ex.
new Object()), tot i que habitualment fem que el monitor sigui el mateix objecte sobre el qual volem exercir control d’accés.
Un exemple de mètodes d’instància:
public class SynchronizedCounter {
private int c = 0;
public synchronized void increment() {
c++;
}
public synchronized void decrement() {
c--;
}
public synchronized int value() {
return c;
}
}
Conseqüències:
- Primer, no és possible que dos fils puguin cridar simultàniament a dos mètodes sincronitzats. Les subseqüents crides se suspenen fins que el primer fil acabi amb l’objecte.
- Segon, quan un mètode sincronitzat acaba, estableix una relació happens-before: les crides subseqüents tindran visibles els canvis fets.
Important: dins d’un bloc sincronitzat, cal fer la feina mínima possible: llegir les dades i si cal, transformar-les.
Un exemple de blocs de codi:
public class MsLunch {
private long c1 = 0;
private long c2 = 0;
private Object lock1 = new Object();
private Object lock2 = new Object();
public void inc1() {
synchronized(lock1) {
c1++;
}
}
public void inc2() {
synchronized(lock2) {
c2++;
}
}
}
Aquest mètode permet tenir un gra més fi: pot haver-hi un fil a la zona de codi de lock1 i un altre a la de lock2.
Wait / Notify (guarded lock)
Imaginem que volem esperar fins que es compleixi una condició:
public void waitForHappiness() {
// Simple method, wastes CPU: never do that!
while (!hapiness) {}
System.out.println("Happiness achived!");
}
Això ho podem fer entre fils mitjançant el mètode clàssic de comunicació wait i notify, que permet:
- Esperar fins que una condició que implica dades compartides sigui certa, i
- notificar a altres fils que les dades compartides han canviat, probablement activant una condició per la que esperen altres fils.
Els mètodes són:
- wait(): quan es crida, el fil actual espera fins que un altre fil cridi notify() o notifyall() sobre aquest monitor.
- notify(): desperta un fil qualsevol de tots els que estiguin esperant a aquest monitor.
- notifyAll(): desperta tots els fils que estiguin esperant a aquest monitor.
Els mètodes wait() i notify s’han de cridar des de dins d’un bloc sincronitzat per a l’objecte monitor.
A més, tal i com es comenta a Object, el mètode wait() ha de ser a dins d’un bucle esperant per una condició:
// in one thread:
synchronized (monitor) {
while (!condition) {
monitor.wait();
}
}
// in the other thread:
synchronized (monitor) {
monitor.notify();
}
En el nostre cas:
synchronized (monitor) {
while (!happiness) {
monitor.wait();
}
}
...
synchronized (monitor) {
happiness = true;
monitor.notify();
}
Funcionament del wait / notify

- El fill 1 obté el monitor i comprova si la condició és true per acabar el loop.
- Com que és false, fa wait() i deixa el monitor.
- El fill 2 obté el monitor, canvia la condició a false i fa notify().
- El fill 1 es desperta i passa a ‘ready’, demanant pel monitor.
- Quan l’obté, veu que la condició és true i acaba el loop.
Reentrant Lock
L’ús de synchronized és molt senzill i suficient en molts escenaris. Però hi ha una implementació, ReentrantLock, que permet les següents característiques addicionals:
- Intent de bloqueig: permet provar de bloquejar un lock sense haver d’esperar.
- Locks justos: permeten que els fils s’acabin executant en l’ordre en que han demanat el lock.
- Locks condicionals: permeten que un fil esperi fins que una condició es compleixi.
- Locks amb interrupció: permeten que un fil esperi fins que una condició es compleixi, però que es pugui interrompre.
Aquest podria ser el nostre fil per a l’exemple del comptador:
static class MyRunnable implements Runnable {
SharedObject so;
Lock lock;
MyRunnable(SharedObject so, Lock lock) {
this.so = so;
this.lock = lock;
}
void increment() {
try {
lock.lock();
so.counter ++;
} finally {
lock.unlock();
}
}
@Override
public void run() {
for (int i=0; i<1_000_000; i++) {
increment();
}
}
}
Com es pot veure, el mètode increment() fa servir un lock per a sincronitzar l’accés a la variable compartida. Això és el mateix que fer servir un mètode sincronitzat, però amb la diferència que podem fer servir el lock en un bloc de codi que pot llançar una excepció.
Llibreria Java concurrent
La llibreria java.util.concurrent conté classes útils quan fem concurrència:
- Executors: la interfície Executor permet representar un objecte que executa tasques. ExecutorService permet el processament asíncron, gestionant una cua i executant les tasques enviades segons la disponibilitat dels fils.
- Cues: ConcurrentLinkedQueue, BlockingQueue.
- Sincronitzadors: els clàssics semàfors (Semaphore), CountDownLatch.
- Col·leccions concurrents: per exemple,
ConcurrentHashMap, o els mètodes de CollectionssynchronizedMap(),synchronizedList()isynchronizedSet(). - Variables que permeten operacions atòmiques sense bloqueig al paquet java.util.concurrent.atomic: AtomicBoolean, AtomicInteger, etc.
Sempre és preferible utilitzar aquestes classes que els mètodes de sincronització wait/notify, perquè simplifiquen la programació. De la mateixa manera que és millor utilitzar executors i tasques que fils directament.
Tasques i executors
La majoria d’aplicacions concurrents s’organitzen mitjançant tasques. Una tasca realitza una feina concreta. D’aquesta forma, podem simplificar el disseny i el funcionament.
Veiem una possible solució per a la gestió de connexions a un servidor. Suposem que tenim un mètode, atendrePeticio(), que atén una petició web.
Execució seqüencial
try (ServerSocket socket = new ServerSocket(5508)) {
while (true) {
Socket client = socket.accept();
handleSession(client);
}
}
Un fil per cada petició
try (ServerSocket socket = new ServerSocket(5508)) {
while (true) {
Socket client = socket.accept();
Runnable tasca = new Runnable() {
@Override
public void run() {
handleSession(client);
}
};
new Thread(tasca).start();
}
}
Grup compartit de fils
int NFILS = 100;
Executor executor = Executors.newFixedThreadPool(NFILS);
try (ServerSocket socket = new ServerSocket(5508)) {
while (true) {
final Socket client = socket.accept();
Runnable tasca = new Runnable() {
public void run() {
handleSession(client);
}
};
executor.execute(tasca);
}
}
En aquesta solució hem introduït la interfície Executor:
public interface Executor {
void execute(Runnable command);
}
És un objecte que permet executar Runnables. Internament, el que fa és executar tasques de forma asíncrona, creant un fil per cada tasca en execució, i retornant el control al fil que crida el seu mètode execute. Les tasques poden tenir quatre estats:
- Creada
- Enviada
- Iniciada
- Completada
Els Executors es poden crear des de la classe amb mètodes estàtics Executors. Aquesta classe retorna una subclasse de Executor, l’ExecutorService. Aquesta subclasse usa el patró Thread Pool, que reutilitza un nombre màxim de fils entre una sèrie de tasques a una cua.
Un ExecutorService ha de parar-se sempre amb el mètode shutdown(), que para tots el fils del pool.
Tasques amb resultats
Algunes tasques retornen resultats. Per implementar-les, podem utilitzar les interfícies Callable i Future:
public interface Callable<V> {
V call() throws Exception;
}
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException, CancellationException;
V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, CancellationException, TimeoutException;
}
Callable<V> permet executar la tasca i retornar un valor del tipus V. Per tal de poder executar-la, necessitem un ExecutorService. En particular, els seus dos mètodes:
Future<?> submit(Runnable task)<T> Future<T> submit(Callable<T> task)
Aquests permeten executar un Runnable / Callable i retornen un Future, que és un objecte que permet obtenir el resultat en diferit mitjançant el mètode get() (bloqueig) o get(long timeout, TimeUnit unit) (bloqueig per un temps).
També podem cancel·lar la tasca mitjançant cancel(boolean mayInterruptIfRunning): el paràmetre diu si es vol interrompre també si ja ha començat.
Els ExecutorService poden crear-se mitjançant la mateixa classe que hem vist abans, Executors.
A continuació, un exemple de funcionament. Com canvia l’execució si fem Executors.newFixedThreadPool(2)?
public class CallablePlay {
static final Logger logger = LoggerFactory.getLogger(CallablePlay.class);
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(1); // <-- change this to 2
Future<String> f1 = executor.submit(new ToUpperCallable("hello"));
Future<String> f2 = executor.submit(new ToUpperCallable("world"));
try {
long millis = System.currentTimeMillis();
logger.info("main {} {} in millis: {}",
f1.get(), f2.get(), (System.currentTimeMillis() - millis));
} catch (InterruptedException | ExecutionException ex) {
logger.error(null, ex);
}
executor.shutdown();
}
private static final class ToUpperCallable implements Callable<String> {
private String word;
public ToUpperCallable(String word) {
this.word = word;
}
@Override
public String call() throws Exception {
String name = Thread.currentThread().getName();
logger.info("{} calling for {}", name, word);
Thread.sleep(2500);
String result = word.toUpperCase();
logger.info("{} result {} => {}", name, word, result);
return result;
}
}
}
A Java 7 es va introduir el framework fork/join.
A Java 8 es va introduir el CompletableFuture, que permet combinar futurs i gestionar millor els errors que es produeixen. Un exemple és l’ús del mètode complete per a completar un futur, en un altre fil:
CompletableFuture<String> cf = new CompletableFuture<>();
new Thread(() -> {
logger.info("sleeping...");
Threads.rest(2500);
logger.info("completing...");
cf.complete("hello!");
}, "writer").start();
new Thread(() -> {
try {
logger.info("getting result...");
String result = cf.get();
logger.info("result: {}", result);
} catch (InterruptedException | ExecutionException e) {
logger.error("Error getting result", e);
}
}, "reader").start();
A continuació es demostra una cadena d’etapes (stages) de CompletableFuture. Cada etapa transforma o consumeix el resultat, tancant finalment l’executor:
ExecutorService es = Executors.newCachedThreadPool();
CompletableFuture.supplyAsync(() -> { // Supplier
Threads.rest(250);
return "hello!";
}, es).thenApply(s -> { // Function
Threads.rest(250);
return s.toUpperCase();
}).thenAccept(s -> { // Consumer
Threads.rest(250);
logger.info("got {}", s);
}).thenRun(() -> { // Runnable
Threads.rest(250);
logger.info("done here!");
es.shutdown();
});
Pas de missatges
- Model de programació síncron i asíncron
- Comunicació asíncrona
- Gestió síncrona de peticions
- Gestió asíncrona de peticions
- Exemples
- Programació i sistemes reactius
El pas de missatges pot implementar-se:
- Dins d’un procés, mitjançant fils. Utilitzant buffers o cues, per exemple.
- Entre processos. Habitualment, es fa utilitzant el paradigma client/servidor i mitjançant xarxes. Un possible mecanisme és l’ús de sòcols, com es podrà veure a la UF Sòcols i serveis. En aquesta comunicació no hi ha compartició de dades mutables, però pot passar que múltiples clients accedeixin simultàniament a un mateix servidor.
En el diagrama pot veure’s una implementació entre processos.

Model de programació síncron i asíncron
La comunicació entre les dues parts es pot realitzar de forma síncrona o de forma asíncrona, segons hi hagi un bloqueig E/S (entrada/sortida).

Comes pot veure al diagrama, en la forma síncrona el client espera la resposta del servidor (bloqueig E/S), i mentrestant no fa res. A la forma asíncrona envia la petició, continua treballant i en un moment donat rep la resposta (sense bloqueig E/S).
Quina forma és més convenient? Depèn de les circumstàncies. La forma síncrona és més fàcil d’implementar, però l’asíncrona permet millorar el rendiment del sistema introduint la concurrència.
Comunicació asíncrona
Les peticions asíncrones han de permetre al client conèixer el resultat a posteriori. Alguns solucions possibles:
- Cap: el client només pot saber com va anar fent una o diverses consultes posteriors (polling).
- Una crida de codi: quan acaba la petició, el servidor fa una crida al codi. Podria implementar-se mitjançant callbacks.
- Un missatge: quan acaba la petició, el servidor envia un missatge que pot rebre el client. Aquest missatge pot viatjar en diferents protocols, i se sol implementar mitjançant algun tipus de middleware. Habitualment, els missatges van a parar a cues, que després gestionen els servidors.
Gestió síncrona de peticions
Quan utilitzem el model síncron (amb bloqueig), un sol fil no pot gestionar diverses peticions simultànies. Això vol dir que necessitem crear un fil per gestionar cada petició i retornar la resposta. En diem arquitectura basada en fils.
Habitualment, es limita el nombre de fils que es permeten gestionar simultàniament per evitar el consum excessiu de recursos.

Gestió asíncrona de peticions
Es reprodueix el patró productor-consumidor: els productors són l’origen dels esdeveniments, i només saben que un ha ocorregut; mentre els consumidors necessiten saber que hi ha un nou esdeveniment, i l’han d’atendre (handle). En diem arquitectura basada en esdeveniments.
Algunes tècniques per implementar el servei:
- El patró reactor: les peticions es reben i es processen de forma síncrona, en un mateix fil. Funciona si les peticions es processen ràpidament.
- El patró proactor: les peticions es reben i es divideix el processament asíncronament, introduint concurrència.
A Java tenim Vert.x, una implementació multireactor (amb N bucles d’esdeveniments).

Una altra tècnica per a gestionar peticions asíncrones és el model d’actors. Aquest model permet crear programes concurrents utilitzant actors no concurrents.
- Un actor és una unitat de computació lleugera i desacoblada.
- Els actors tenen estat, però no poden accedir a l’estat d’altres actors.
- Es pot comunicar amb altres actors mitjançant missatges asíncrons immutables.
- L’actor processa els missatges seqüencialment, evitant contenció sobre l’estat.
- Els missatges poden estar distribuïts per la xarxa.
- No es pressuposa cap ordre concret en els missatges.

A Java, tenim un exemple de llibreria: Akka.
Exemples
Una forma d’implementar-lo és passar missatges entre fils mitjançant l’ús d’una cua sincronitzada. Pot haver-hi un o més productors i un o més consumidors. La cua ha de ser thread-safe. A Java, les implementacions de BlockingQueue, ArrayBlockingQueue i LinkedBlockingQueue, en són exemples. Els objectes a aquestes cues han de ser d’un tipus immutable.
Buffer asíncron (cua)
En aquest exemple, un fil productor envia treballs (1, 2, 3, 4) a un fil consumidor mitjançant una cua thread-safe. La mida màxima de la cua es 2.
Les accions són:
- put (prod): afegir un treball, esperant si no hi ha prou espai.
- take (cons): llegir un treball per processar-lo, i esperar si no hi ha cap.

Flux de crides de la impressora asíncrona
De vegades, les peticions fan referència a un recurs compartit que no permet el seu ús per més d’un client alhora. En aquests casos, es pot implementar una cua que gestioni les peticions de forma asíncrona:
- El client realitza la petició asíncrona, i més endavant pot rebre la resposta o confirmació de la petició.
- El servidor registra la petició en una cua, que va atenent per ordre a un fil independent.
La impressora és un únic fil (servidor) que va llegint els treballs afegits a la cua per diferents usuaris (fils), i atenent-los.
També podríem tenir més d’una cua, si hi ha la possibilitat de tenir més d’un punt per atendre les peticions (diverses impressores).

Programació i sistemes reactius
La programació passiva és la tradicional als dissenys OO: un mòdul delega en un altre per a produir un canvi al model.
L’alternativa plantejada es diu programació reactiva, on utilitzem callbacks per a invertir la responsabilitat.
El terme “reactiu” s’utilitza en dos contextos:
- La programació reactiva està basada en esdeveniments (event-driven). Un esdeveniment permet el registre de diversos observadors. Habitualment funciona de forma local.
- Els sistemes reactius generalment es basen en missatges (message-driven) amb un únic destí. Es corresponen més sovint a processos distribuïts que es comuniquen a través d’una xarxa, potser com a microserveis que cooperen.
En l’exemple de la cistella de la compra, podem veure com implementar-ho amb programació passiva i reactiva:
- Amb passiva, la cistella actualitza la factura. Per tant, la cistella és la responsable del canvi i depèn de la factura.
- Amb reactiva, la factura rep un esdeveniment de producte afegit i s’actualiza a si mateixa. La factura depèn de la cistella, ja que li ha de dir que vol sentir els seus esdeveniments.

Pros i contres:
- La programació reactiva permet entendre millor com funciona un mòdul: només cal mirar al seu codi, ja que és responsable d’ell mateix. Amb la passiva és més difícil, ja que cal mirar-se els altres mòduls que el modifiquen.
- Per altra banda, amb programació passiva és més fàcil entendre a quins mòduls afecta un: mirant quines referències es fan. Amb programació reactiva cal mirar-se quins mòduls generen un cert esdeveniment.
La programació reactiva és asíncrona i sense bloqueig. Els fils que busquen recursos compartits no bloquegen l’espera que el recurs estigui disponible. En el seu lloc, continuen la seva execució i són notificats després quan el servei s’ha completat.
Les extensions reactives permeten que llenguatges imperatius, com Java, puguin implementar programació reactiva. Ho fan utilitzant programació asíncrona i streams observables, que emeten tres tipus d’esdeveniments als seus subscriptors: següent, error i completat.
Des de Java 9 s’han definit els streams reactius utilitzant el patró Publish-Subscribe (molt semblant al patró observador) mitjançant les interfícies Flow. Les implementacions més utilitzades són Project Reactor (p. ex. Spring WebFlux) i RxJava (p. ex. Android).
Per altra banda, un sistema reactiu és un estil d’arquitectura que permet que diverses aplicacions puguin comportar-se com una sola, reaccionant al seu entorn, mantenint-se al corrent els uns dels altres, i permetent la seva elasticitat, resiliència i responsivitat basats (habitualment) en cues de missatges dirigits a receptors concrets (vegeu el Reactive Manifesto). Una aplicació dels sistemes reactius són els microserveis.
Tant els patrons reactor/proactor com el model d’actors permeten implementar sistemes reactius.
Sòcols i serveis
Referències
- OSI Model
- HTTP (Mozilla)
- HttpURLConnection
- reqbin.com
- A guide to Java sockets
- Do a Simple HTTP Request in Java
- Core Java Networking (eugenp)
- Read an InputStream using the Java Server Socket
- REST vs Websockets (baeldung)
- A Guide to the Java API for WebSocket (baeldung)
- All About Sockets (The Java Tutorials)
- SSL Handshake Failures (baeldung)
- HTTP: The Protocol Every Web Developer Must Know (part 1)
- HTTP: The Protocol Every Web Developer Must Know (part 2)
- REST API Tutorial
- Blocking I/O and non-blocking I/O
- A Guide to NIO2 Asynchronous Socket Channel
- Java NIO Tutorial (Jenkov)
- How Single-Page Applications Work
- What Is a Single Page Application and Why Do People Like Them so Much?
- Guía práctica para la publicación de Datos Abiertos usando APIs
- Local-first software
Protocols
Els protocols d’Internet tenen més d’un model. El model OSI ens dona una organització per capes:
- Física: transmissió i recepció de bits sobre el mitjà físic.
- Enllaç: transmissió fiable de trames entre dos nodes. PPP.
- Xarxa: transmissió de paquets sobre una xarxa multi-node, amb adreçament, encaminament i control de tràfic. IP.
- Transport: transmissió de segments de dades entre punts d’una xarxa. TCP, UDP.
- Sessió: gestió de sessions.
- Presentació: traducció dels protocols cap a una aplicació. MIME, SSL.
- Aplicació: APIs d’alt nivell. HTTP, Websockets.

Com a programadors, podem involucrar-nos a diferents nivells del model:
- Construint protocols basats en TCP, UDP mitjançant Java Sockets (nivell 4 i mig).
- Accedint a aplicacions web HTTP (nivell 7), implementades als ports 80 o 443 (segur).
Els protocols de nivell 4 i mig estàndards es poden veure a aquesta llista.
IP versió 4
Les adreces IPv4 utilitzen 32 bits, i tenen rangs assignats a diferents propòsits:
- Classe A: de 1.x.x.x a 127.x.x.x (subnet mask 255.0.0.0). El rang 127.x.x.x està reservat per al loopback.
- Classe B: de 128.0.x.x a 191.255.x.x (subnet mask 255.255.0.0).
- Classe C: de 192.0.0.x a 223.255.255.x (subnet mask 255.255.255.0).
- Classe D: de 224.0.0.0 a 239.255.255.255. Reservades per a multicasting.
- Classe E: de 240.0.0.0 a 255.255.255.254. Reservades per a recerca.
D’aquestes, es consideren IP privades:
- 10.0.0.0 a 10.255.255.255
- 172.16.0.0 a 172.31.255.255
- 192.168.0.0 a 192.168.255.255
Aquests són els tipus d’encaminaments disponibles:
Unicast: adreces de classe A, B i C. Transport TCP i UDP.

Broadcast: Adreça del host (sufix) amb tot 1’s. Transport UDP. No travessen els encaminadors, i les reben totes les màquines. No disponible a IPv6.

Multicast: adreces de classe D. Transport UDP. Una màquina ha d’escoltar per rebre la comunicació. Disponible a xarxes locals.

IP versió 6
IPv6 és la versió 6 del Protocol d’Internet (IP), i està dissenyat per substituir l’actual IPv4 a internet, però encara no està suportat completament. Les adreces utilitzen 128 bits, que es mostren en grups de 4 dígits hexa.
xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx
IPv6 no té classes. Quan es fa subxarxes, la notació /NN indica la longitud del prefix de xarxa. Per exemple:
2001:0db8:1234::/64
La notació :: indica un o més grups de valor 0. Només hi pot haver un :: en una adreça.
El loopback es representa com ::1/128 o, simplement, ::1.
Els tipus d’encaminament d’IPv6 són unicast, multicast i anycast (el tipus broadcast no existeix a IPv6). Anycast permet enviar un missatge a un group on només contesta un, el més proper.
Disseny de protocols
Classificació dels protocols
Els protocols es poden classificar en base a diversos criteris:
- La direcció:
- Unidireccional: UDP, SSE
- Bidireccional: WebSocket, QUIC
- El patró de comunicació:
- Petició–Resposta: HTTP, gRPC
- Pub/Sub: Kafka, MQTT
- Streaming: WebSocket, SSE
- Negociació: TLS, SSH
- El transport:
- TCP: HTTP, SSH
- UDP: DNS, VoIP
- El format:
- Text: JSON, XML
- Binari: Protobuf, AMQP
- Si tenen estat:
- Sense estat: REST, DNS
- Amb estat: WebSocket, FTP
- La seguretat:
- Sense xifrar: HTTP
- Xifrat: HTTPS, SSH
Introducció al disseny
Els protocols regeixen la comunicació entre dues parts. Un protocol ben dissenyat garanteix fiabilitat, claredat i extensibilitat. El disseny del protocol implica definir:
- Format del missatge: Estructura de peticions i respostes.
- Flux de treball: seqüència d’interaccions entre les dues parts.
- Conversió de flux de bytes: codificació i descodificació de missatges per a la transmissió.
En la comunicació de xarxa, les dades es transmeten com un flux de bytes. Sense una estructura definida, és impossible determinar on acaba un missatge i on comença el següent, o com interpretar el contingut d’un missatge. Un protocol preveu:
- Límits: marcadors per identificar l’inici i el final dels missatges.
- Regles de descodificació: instruccions per interpretar el flux de bytes en brut en dades significatives.
- Coherència: un format estandarditzat per a la comunicació.
Per exemple, un client que envia una sol·licitud d’inici de sessió i un servidor que respon ha d’acordar com codificar el nom d’usuari, la contrasenya i l’estat de resposta en un flux de bytes, així com com tornar-lo a descodificar.
Objectius:
- Estandarditzar la comunicació entre dues parts.
- Minimitzar errors i ambigüitats.
- Admet camps de dades flexibles i de longitud variable.
Disseny d’alt nivell
Abans de dissenyar el protocol, identificar:
- El servei que s’ha d’implementar (p. ex., transferència de fitxers, autenticació o xat).
- Tipus de peticions i respostes necessàries.
A continuació, descriure la interacció típica:
- Configuració de la connexió: com dues parts estableixen una connexió.
- Intercanvi petició-resposta: defineix la seqüència de missatges.
- Desactivació de la connexió: defineix com acaba la connexió.
Un missatge del protocol pot tenir:
- Capçalera, metadades que defineixen el tipus de missatge i què esperem.
- Càrrega útil o payload, amb el contingut real del missatge. Poden contenir camps de mida fixa o mida variable (amb prefixos de longitud).
Exemple de missatge:
| Camp | Descripció |
|---|---|
| Longitud de la capçalera | Mida fixa, indica la mida de la capçalera (en bytes). |
| Tipus de missatge | Especifica el tipus de missatge (p. ex., sol·licitud, resposta). |
| Longitud de càrrega útil | Mida de la càrrega útil en bytes. |
| Càrrega útil | Camp de mida variable que conté les dades reals. |
Exemple de capçalera:
- Longitud de la capçalera: 2 bytes
- Tipus de missatge: 1 byte (p. ex., 0x01 per a la sol·licitud, 0x02 per a la resposta)
- Longitud de càrrega útil: 4 bytes (sencer big-endian)
Exemple de payload per a inici de sessió:
| Camp | Tipus | Llargada |
|---|---|---|
| Longitud del nom d’usuari | Sencer | 2 bytes |
| Nom d’usuari | Cadena | Variable |
| Longitud de la contrasenya | Sencer | 2 bytes |
| Contrasenya | Cadena | Variable |
El procés de codificació i decodificació transforma dades de l’àmbit de la programació en fluxos de dades en fluxos de bytes, i de nou a dades. Aquest procés s’anomena també serialització i deserialització, i hauria de ser independent del llenguatge de programació, el sistema operatiu o altres condicionants.
Disseny de protocols per a TCP i UDP
En implementar un protocol, és essencial entendre les característiques específiques del protocol de transport subjacent.
TCP (Protocol de control de transmissió)
TCP proporciona un lliurament fiable, ordenat i verificat d’errors de fluxos de dades.
Característiques clau de TCP
- Fiabilitat: TCP assegura que tots els missatges s’entreguen sense pèrdua.
- Preservació de l’ordre: les dades arriben al receptor en el mateix ordre en què es van enviar.
- Orientat al flux: els missatges es transmeten com un flux continu de bytes, que requereixen mecanismes addicionals per definir els límits del missatge.
Consideracions sobre l’estructura del missatge
- Límits explícits: cal utilitzar prefixos de longitud o delimitadors per separar els missatges dins del flux.
- Reassemblatge de fragments: els missatges han d’enviar-se en unitats de transmissió i, internament, l’API de Java s’encarrega de l’assemblatge i reassemblatge quan superen la mida d’una unitat.
- Amb estat: la comunicació TCP és orientada a connexió i manté l’estat entre client i servidor durant tota la sessió. Això permet assegurar l’ordre dels missatges i detectar la pèrdua o duplicació de dades.
UDP (Protocol de datagrama d’usuari)
UDP és un protocol sense connexió que ofereix una comunicació més ràpida però menys fiable.
Característiques clau d’UDP
- Sense fiabilitat: es poden perdre missatges o lliurar-se fora de servei sense cap retransmissió automàtica.
- Sense connexió: no cal establir una connexió abans d’enviar dades, per la qual cosa és més ràpid que TCP.
- Sense preservació de l’ordre: UDP no garanteix que els missatges arribin en el mateix ordre en què es van enviar.
- Baixa sobrecàrrega: les capçaleres UDP són més petites en comparació amb les capçaleres TCP, la qual cosa la fa més eficient pel que fa a l’ús de l’ample de banda.
Consideracions sobre l’estructura del missatge:
- Sense límits incorporats: com que UDP està orientat a missatges, cada paquet UDP és un sol missatge. Si s’han enviar diversos missatges en un paquet, cal definir límits a nivell d’aplicació (p. ex., utilitzant delimitadors o prefixos de longitud).
- Límits de mida dels missatges: els paquets UDP solen tenir una mida limitada (normalment uns 65.535 bytes), però pot ser que es necessiti fragmentació a nivell d’aplicació per a missatges més grans.
- Sense estat: la comunicació amb UDP és connectionless, és a dir, no es manté cap estat entre els extrems de la comunicació. Cada missatge (datagrama) s’envia de manera independent, i no hi ha garantia d’entrega, ni d’ordenació, ni de detecció de duplicats. És responsabilitat de l’aplicació, si cal, implementar mecanismes de control o fiabilitat.
Escollir entre TCP i UDP
Quan es dissenya un protocol, l’elecció entre TCP i UDP depèn de diversos factors:
- Requisits de fiabilitat: si s’ha de garantir l’entrega de missatges (per exemple, transaccions financeres, transferències de fitxers), TCP és la millor opció a causa de les seves capacitats de verificació d’errors i retransmissió.
- Necessitats de rendiment: si una latència baixa és crítica i es pot tolerar la pèrdua ocasional de missatges (per exemple, la transmissió de veu o de vídeo en temps real), UDP pot ser una millor opció a causa de la seva menor sobrecàrrega i de la seva entrega més ràpida.
- Mida i freqüència del missatge: per a protocols amb missatges grans i poc freqüents (p. ex., transferència de fitxers), el disseny orientat al flux de TCP funciona bé. Per a missatges més petits i freqüents (per exemple, consultes DNS), la senzillesa i l’eficiència d’UDP són avantatjoses.
Protocol HTTP
HTTP és un protocol de nivell aplicació per a sistemes col·laboratius i distribuïts. És el component principal de la web, gràcies a l’ús de documents d’hipertext. HTTP/1.1, la versió actual, està implementat mitjançant TCP al transport. La versió 2 ja està estandaritzada, i la 3 funcionarà sobre UDP.
La versió segura d’HTTP es diu HTTPS, o també HTTP sobre TLS, el protocol criptogràfic per a la transmissió segura.
Sessió
Una sessió és una seqüència de peticions/respostes. Comença mitjançant l’establiment d’una connexió TCP a un port d’un servidor (habitualment 80). El servidor contesta habitualment amb un codi, del tipus “HTTP/1.1 200 OK”, i amb un cos, que normalment conté el recurs demanat.
HTTP es un protocol sense estat, tot i que algunes aplicacions utilitzen mecanismes per emmagatzemar informació. Per exemple, les cookies.
Missatges
Una petició conté, habitualment:
- Una línia de petició, amb un mètode. Exemple: GET /images/logo.png HTTP/1.1
- Camps de la capçalera de la petició. Exemple: Accept-Language: ca
- Una línia buida.
- Un cos opcional. Exemple: per fer un POST.
Mètodes de petició:
- GET: el mètode habitual per obtenir un recurs. No té cos.
- POST: el mètode utilitzat per enviar un cos al servidor. S’utilitza als formularis.
- PUT, DELETE, TRACE, OPTIONS, CONNECT, PATCH son altres mètodes utilitzats.
Una resposta conté:
- Una línia d’estat. Exemple: HTTP/1.1 200 OK.
- Camps de la capçalera de la resposta. Exemple: Content-Type: text/html
- Una línia buida.
- Un cos opcional. Exemple: per a un GET, el contingut requerit.
Els codis d’estat poden ser del tipus:
- Informació (1XX).
- Èxit (2XX). Exemple: 200 OK.
- Redirecció (3XX). Exemple: 301 Moved Permanently.
- Error de client (4XX). Exemple: 404 Not Found.
- Error de servidor (5XX). Exemple: 500 Internal Server Error.
Podem utilitzar el programa telnet per conectar-nos a un servidor web HTTP i enviar una comanda GET.
$ telnet maripili.es 80
Trying 217.160.0.165...
Connected to maripili.es.
Escape character is '^]'.
GET / HTTP/1.0
Host: maripili.es
Això provoca la resposta del servidor:
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Connection: close
Date: Wed, 16 Feb 2022 08:12:31 GMT
Server: Apache
<!DOCTYPE html>
<html lang="es">
...
</html>
Connection closed by foreign host.
Eines
Tenim tres eines per depurar protocols HTTP: netcat, curl i l’inspector dels navegadors.
Netcat permet connectar-se a un port i fer una conversa, utilitzant les canonades. Si s’indica -u utilitza UDP, si no, TCP. Per exemple, per a accedir al servei echo de la nostra màquina:
nc localhost 7
CURL permet obtenir la resposta d’una URL a la xarxa.
$ curl -I http://maripili.es
(GET, veure headers)
HTTP/1.1 200 OK
Date: Fri, 05 Apr 2019 05:03:23 GMT
Server: Apache
X-Logged-In: False
P3P: CP="NOI ADM DEV PSAi COM NAV OUR OTRo STP IND DEM"
Cache-Control: no-cache
Pragma: no-cache
Set-Cookie: 4af180c8954a0d5a1965b5b1b23ccbc5=pc1jg8f5kqigd71iec5a5lm7k5; path=/
X-Powered-By: PleskLin
Content-Type: text/html; charset=utf-8
$ curl http://maripili.es
(GET, contingut de la pàgina web)
$ curl -v http://maripili.es
(GET, headers i contingut)
$ curl -d "key1=val1&key2=val2" http://maripili.es/contacto/
(POST)

Implementació a Java
URL i HttpURLConnection
La classe URL fa referència a un recurs a la web. Un recurs genèric pot tenir la següent forma.

Veiem un exemple per al protocol HTTP:
https://www.example.com/test?key1=value1&key2=value2
En aquest cas, tenim que:
- l’esquema és https
- el host és www.example.com
- el port és 80, però no s’indica, ja que és el valor per defecte al protocol HTTP
- el camí (path) és test
- la query és key1=value1&key2=value2
A Java es pot construir una URL amb:
URL url = new URL(String spec)
Un cop fet això, podem accedir a cada part de l’URL amb els mètodes getHost(), getPath(), getPort(), getProtocol(), getQuery(), etc.
Els dos mètodes més importants per interactuar amb l’URL són:
URLConnection openConnection(): retorna una connexió al recurs remot.InputStream openStream(): retorna un InputStream per a llegir el recurs remot.
La classe URLConnection és abstracta, i si hem accedir a un recurs HTTP llavors l’objecte serà una instància de HttpURLConnection.
openStream
Per llegir una pàgina web que es trobi a l’URL d’una cadena anomenada urlText, podem fer:
URL url = new URL(urlText);
BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream()));
Per llegir un arxiu:
BufferedInputStream in = new BufferedInputStream(new URL(urlText).openStream());
openConnection
Amb openConnection podem accedir als mètodes del protocol HTTP i els codis d’estat que es retornen o el tipus de contingut.
Aquest és un mètode GET:
URL url = new URL(urlText);
HttpURLConnection httpConn = ((HttpURLConnection) url.openConnection());
httpConn.setRequestMethod("GET"); // opcional: GET és el mètode per defecte
int responseCode = httpConn.getResponseCode();
String contentType = httpConn.getContentType();
BufferedReader in = new BufferedReader(new InputStreamReader(httpConn.getInputStream()));
// falta llegir in: resposta del servidor
Aquest és un mètode POST:
URL url = new URL(urlText);
HttpURLConnection httpConn = ((HttpURLConnection) url.openConnection());
httpConn.setRequestMethod("POST");
httpConn.setDoOutput(true);
OutputStreamWriter out = new OutputStreamWriter(httpConn.getOutputStream());
out.write("propietat1=valor1&propietat2=valor2"); // valors dels paràmetres del POST
out.close();
int responseCode = httpConn.getResponseCode();
String contentType = httpConn.getContentType();
BufferedReader in = new BufferedReader(new InputStreamReader(httpConn.getInputStream()));
// falta llegir in: resposta del servidor
Sòcols
- TCP i UDP
- Protocol exemple: ECHO
- Protocol exemple: SMTP
- Comunicació TCP
- Comunicació UDP
- Timeouts
- Tancament
- Comunicació asíncrona
Un sòcol és un enllaç de doble sentit que permet comunicar dos programaris que són a la xarxa.
Aquests dos programaris fan dues funcions: la del client i la del servidor. El servidor proveeix algun servei des d’un lloc conegut (adreça IP + port), i el client accedeix a aquest servei. Aquest servei ha d’implementar un protocol ben definit, sigui un estàndard o un de dissenyat a mida.
Els números de ports són:
- El rang 0 a 1023 són els ports coneguts (well-known) o de sistema. En Linux, cal ser administrador per tenir un servei en aquests ports.
- El rang 1024-49151 són els ports registrats, assignats per IANA.
- El rang 49152–65535 són ports dinàmics o privats, o de vida curta.
Com que els dos programaris treballen en l’àmbit del protocol, al codi dels dos programaris no hi ha dependències mútues. Però és habitual que qui implementa el protocol proveeixi d’una llibreria de client per poder accedir al servei. Això permet reduir el codi que un client ha d’escriure, i assegura que utilitzarà correctament el protocol. A Java, la llibreria de client es materialitza mitjançant un arxiu jar i una documentació d’ús.
TCP i UDP
Es poden utilitzar els protocols TCP o UDP. TCP està orientat a connexió, i UDP no. Això vol dir que TCP requereix un pas previ de connexió entre el client i el servidor per tal de comunicar-se. Un cop establerta la connexió, TCP garanteix que les dades arribin a l’altre extrem o indicarà que s’ha produït un error.
En general, els paquets que han de passar en l’ordre correcte, sense pèrdues, utilitzen TCP, mentre que els serveis en temps real on els paquets posteriors són més importants que els paquets més antics utilitzen UDP. Per exemple, la transferència d’arxius requereix una precisió màxima, de manera que normalment es fa mitjançant TCP, i la conferència d’àudio es fa freqüentment a través d’UDP, en què pot ser que no es notin les interrupcions momentànies.
TCP necessita uns paquets de control per a establir la connexió en tres fases: SYN, SYN + ACK i ACK. Cada paquet enviat es contesta amb un ACK. I finalment, es produeix una desconnexió des de les dues bandes amb FIN + ACK i ACK.
UDP, en canvi, només transmet els paquets de petició / resposta, sense cap control sobre la transmissió.
Protocol exemple: ECHO
A continuació es pot veure la visualització del protocol ECHO amb Wireshark, tant per a la implementació TCP com la UDP.
Captura TCP (petició i resposta)

Captura UDP (petició i resposta)

Protocol exemple: SMTP
SMTP és un protocol que funciona al port 25, sobre TCP. El client envia comandes, i el servidor respon amb un codi d’estat.
A continuació, veiem una conversa (C: client / S: servidor). Tota aquesta conversa es manté sobre una connexió oberta.
C: <client connects to service port 25>
C: HELO snark.thyrsus.com la máquina que envia s'identifica
S: 250 OK Hello snark, glad to meet you el receptor accepta
C: MAIL FROM: <esr@thyrsus.com> identificació de l'usuari que envia
S: 250 <esr@thyrsus.com>... Sender ok el receptor accepta
C: RCPT TO: cor@cpmy.com identificació del destí
S: 250 root... Recipient ok el receptor accepta
C: DATA
S: 354 Enter mail, end with "." on a line by itself
C: Scratch called. He wants to share
C: a room with us at Balticon.
C: . final de l'enviament multi-línia
S: 250 WAA01865 Message accepted for delivery
C: QUIT l'emissor s'acomiada
S: 221 cpmy.com closing connection el receptor es desconnecta
C: <client hangs up>
Comunicació TCP
Java té dues classes del paquet java.net que ho permeten:
Socket: implementació del sòcol client, que permeten comunicar dos programaris a la xarxa.ServerSocket: implementació del sòcol servidor, que permet escoltar peticions rebudes des de la xarxa.
El funcionament es reflecteix en les següents dues imatges: primer el client demana una connexió, i després el servidor l’accepta, i s’estableix.

Servidor a majúscules
Aquest servidor escolta línies de text i retorna la versió en majúscules.
La comunicació comença amb la petició de connexió del client, i l’acceptació del servidor. Aquestes dues accions creen un sòcol compartit del tipus Socket, sobre el qual, tan el client com el servidor, poden utilitzar els mètodes:
getInputStream()getOutputStream()
El client pot enviar cadenes de text, que el servidor convertirà a majúscules.
El protocol que ens hem inventat estableix que la comunicació s’acaba quan el client envia una línia buida. a la qual el servidor contesta amb un comiat.

Aquest podria ser un codi del servidor que implementa el protocol descrit utilitzant TCP.
ServerSocket serverSocket = new ServerSocket(PORT);
Socket clientSocket = serverSocket.accept();
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
out.println("hola!");
String text;
while ((text = in.readLine()).length() > 0)
out.println(text.toUpperCase());
out.println("adeu!");
clientSocket.close();
serverSocket.close();
Pots provar-ho mitjançant la comanda netcat (nc).
Com seria el protocol d’aquest servei? En pseudocodi:
- Quan et connectes al servidor, envia una línia amb una salutació.
- Per cada línia que envies, et retorna la mateixa línia en majúscules.
- Quan envies una línia en blanc, et contesta amb el comiat, i es desconnecta.
A continuació, es pot veure un client que accedeix a aquest servei, implementant aquest protocol.
Socket clientSocket = new Socket(HOST, PORT);
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
String salutacio = in.readLine();
System.out.println("salutacio: " + salutacio);
for (String text: new String[]{"u", "dos", "tres"}) {
out.println(text);
String resposta = in.readLine();
System.out.println(text + " => " + resposta);
}
out.println();
String comiat = in.readLine();
System.out.println("comiat: " + comiat);
in.close();
out.close();
clientSocket.close();
Comunicació basada en text
Als exemples que hem vist, PrintWriter i BufferedReader són els objectes que permeten utilitzar cadenes de text sobre l’OutputStream i l’InputStream respectivament, que són mitjans de comunicació binària.
El més correcte en aquests casos seria indicar quin és el Charset amb que codifiquem els Strings. Si volguèssim utilitzar UTF-8, seria així:
Charset UTF8 = StandardCharsets.UTF_8;
PrintWriter out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), UTF8), true);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream(), UTF8));
Relacionat amb el charset, tenim la conversió des de i cap a String a partir de dades binàries, que podem realitzar a UDP. Aquestes són les dues operacions:
byte[] strBytes = "Hola, món!".getBytes(UTF8);
String str = new String(strBytes, UTF8);
Comunicació UDP
Amb UDP no hi ha connexió: simplement s’envien paquets (Datagrames) amb destinació un servidor UDP. Si volem respondre, cal conèixer l’adreça i port destí, que pot obtenir-se del paquet rebut.
Un servidor es pot crear amb:
DatagramSocket socket = new DatagramSocket(PORT)
El client funciona exactament igual, però el socol es crea amb:
DatagramSocket socket = new DatagramSocket();
Per rebre un paquet de mida màxima MIDA:
byte[] buf = new byte[MIDA];
DatagramPacket paquet = new DatagramPacket(buf, MIDA);
socket.receive(paquet);
// dades a paquet.getData() i origen a paquet.getAddress() i paquet.getPort()
Per enviar un paquet al servidor:
InetAddress address = InetAddress.getByName(HOST);
paquet = new DatagramPacket(buf, MIDA, address, PORT);
socket.send(paquet);
Per enviar un paquet de resposta a un client, hem d’utilitzar el port que hi ha al paquet que ens ha enviat prèviament:
InetAddress address = paquetRebut.getAddress();
int port = paquetRebut.getPort();
DatagramPacket paquetResposta = new DatagramPacket(buf, buf.length, address, port);
socket.send(paquet);
Concurrència
Com podem fer que els servidors acceptin peticions concurrents de diversos clients?
Ho vam veure a la UF “Processos i fils”. Caldria atendre cada petició a un fil diferent. Per exemple, per al cas de TCP:
Executor executor = Executors.newFixedThreadPool(NFILS);
ServerSocket serverSocket = new ServerSocket(PORT);
while (true) {
final Socket clientSocket = serverSocket.accept();
Runnable tasca = new Runnable() {
public void run() {
atendrePeticio(clientSocket);
}
};
executor.execute(tasca);
}
D’aquesta manera, no fem esperar nous clients quan atenem un.
Timeouts
Quan utilitzem sòcols TCP, les operacions de connexió i enviament de dades requereixen la confirmació de l’altra part. Per defecte, l’API que hem vist no té temporitzadors (timeouts), i es bloqueja indefinidament.
També hi ha l’operació UDP de llegir un datagrama, que per defecte es bloqueja esperant la resposta.
Aquestes són les operacions de connexió TCP:
new Socket(host, port...): creació d’un sòcol i connexió sense timeout.new Socket(): creació d’un sòcol no connectat (no hi ha bloqueig).Socket.connect(SocketAddress, timeout): connexió d’un sòcol no connectat amb un timeout.
Un cop creat un sòcol TCP, sigui de client (Socket) o de servidor (ServerSocket), podem canviar la temporització utilitzant Socket.setSoTimeout(int timeout). Quan es cumpleix el temporitzador, es llença una excepció de tipus SocketTimeoutException. Aquest timeout (en mil·lisegons) afecta les següents operacions TCP/UDP:
ServerSocket.accept(): acceptació d’una connexió d’un client al servidor TCP.SocketInputStream.read(): lectura de dades a un sòcol TCP.DatagramSocket.receive(): lectura d’un datagrama UDP.
Tancament
Aquests són alguns aspectes associats al tancament d’un sòcol TCP:
- Podem comprovar si una connexió TCP està tancada amb
Socket.isClosed()iServerSocket.isClosed(). - Si tanquem la connexió d’un sòcol amb
Socket.close(), es tanquen automàticament els seus streamsInputStreamiOutputStream. - Si tanquem la connexió de qualsevol dels seus streams, es tanca automàticament la del sòcol associat.
- Si fem un
ServerSocket.close()i s’està esperant amb unServerSocket.accept(), s’interromprà i hi haurà una SocketException. - Si un sòcol està esperant amb un
SocketInputStream.read()i el sòcol de l’altra banda es tanca, elread()s’interromprà i hi haurà una SocketException. - Si un sòcol està esperant en un canal de text amb un
BufferedReader.read()oBufferedReader.readLine()i el sòcol de l’altra banda es tanca, es retornarà -1 i null, respectivament.
Comunicació asíncrona
Basada en les llibreries NIO. Veure aquest post.
Serveis
Arquitectures
Una aplicació pot veure’s com quatre components: les dades, la lògica d’accés a dades, la lògica de l’aplicació i la presentació. Aquests components es poden distribuir de moltes formes:
- basades en servidor: el servidor fa pràcticament tota la feina. Els clients són molt lleugers.
- basades en client: el client fa pràcticament tota la feina. El servidor només guarda les dades.
- peer-to-peer: les màquines fan de client i servidor i comparteixen la feina, que fan integralment.
- client/servidor: l’arquitectura dominant. La lògica de l’aplicació i d’accés a dades pot estar distribuïda entre client i servidor. Poden tenir múltiples capes: 2, 3, N. Permet integrar aplicacions de diferents proveïdors utilitzant protocols estàndard. Aquesta és l’arquitectura dominant.
L’arquitectura client/servidor està centrada habitualment en les dades: la lògica de negoci s’interposa entre aquestes dades i la interfície d’usuari, habitualment web. Un exemple de patró és el MVC (model/vista/controlador).
Si atenem al criteri d’on es genera l’HTML d’una aplicació web podem tenir:
- El tradicional: l’HTML es genera al servidor.
- El SPA (Single-Page Application): l’HTML es genera al client, i amb el servidor s’intercanvien dades (JSON, habitualment). Al servidor implementem APIs basades en HTTP, que poden compartir-se amb diferents tipus de clients com navegadors o aplicacions per mòbil.
Quan les funcionalitats creixen i s’afegeixen a una solució, tenim el risc de convertir la nostra aplicació en el que s’anomena aplicació monolítica. Algunes solucions arquitecturals proposen solucions:
- Arquitectura de microserveis: proposa serveis completament independents que proporcionen funcionalitats autocontingudes. Tots ells es comuniquen amb protocols lleugers (poden ser heterogenis) basats en REST/HTTP gràcies a un contracte ben establert (API). Basats en l’idea del bucle d’esdeveniments.
- Arquitectura orientada a serveis (SOA): una solució similar a l’anterior, però els serveis es comuniquen utilitzant un middleware més complex anomenat bus de serveis d’empresa (ESB). Basats en coordinació de múltiples serveis al bus.
APIs
El món està cada cop més interconnectat mitjançant APIs que proporcionen serveis. Aquests poden ser públics, per tal d’afegir valor al negoci d’una empresa.
Les APIs es diuen que són gestionades quan tenen un cicle de vida ben definit:
CREADA ➡ PUBLICADA ➡ OBSOLETA ➡ RETIRADA
Només es publiquen un cop estan ben documentades, amb les seves regles de qualitat d’ús, com la limitació d’ús. La forma estàndard i oberta de descriure APIs és mitjançant OpenAPI.
Exemple: API Twitter
HTTP
El protocol HTTP s’implementa a sobre de TCP, habitualment al port 80. Això ens permet implementar un servidor HTTP utilitzant sòcols.
Per escriure el servidor, hem de ser capaços de llegir una petició HTTP i de respondre.
Petició
request = Request-Line
*(<message-header>)
CRLF
[<message-body>]
La Request-Line té el format:
Request-Line = Method URI HTTP-Version
Els mètodes més habituals són GET/POST. La versió, HTTP/1.1. La URI és només la part del camí (path) absolut.
Els headers més habituals són:
- Host (obligatori): especifica el nom de domini del servidor.
- Accept: informa al servidor sobre els tipus de dades que es poden rebre.
El message-body està buit per al mètode GET, i conté els camps d’un formulari per al mètode POST.
Quan és POST, s’envia el header Content-Type amb els valors:
- application/x-www-form-urlencoded: valors codificats en tuples clau-valor separades per &, amb un = entre clau i valor. Els valors no alfanumèrics s’han de codificar en codi percent. En Java es pot fer amb
URLEncoder.encode(query, "UTF-8"). - multipart/form-data: transmisió de dades binàries, per exemple, un arxiu.
- text/plain: format text.
Resposta
response = Status-Line
*(<message-header>)
CRLF
[<message-body>]
El Status-Line té el format:
HTTP-Version Status-Code Reason-Phrase
Els codis d’estat ja els vam veure. La Reason-Phrase és un text llegible que explica el codi.
Els headers més habituals són:
- Content-Type: el tipus MIME (media) retornat. Pot incloure el charset. Exemple:
text/html; charset=UTF-8. - Content-Length: el nombre de bytes del contingut retornat.
- Date: la data del contingut retornat.
- Server: el nom del servidor.
El message-body té el contingut del recurs que s’obté.
Cookies
Les cookies són un mecanisme que permet emmagatzemar parelles clau/valor al navegador des d’un servidor HTTP. Es pot utilitzar per diferents propòsits, per exemple, per identificar una sessió d’un usuari, o bé per seleccionar una preferència de visualització, com pot ser l’idioma.
Hi ha dues capçaleres associades a aquest mecanisme:
- Set-Cookie: capçalera que s’escriu des de la resposta del servidor per a assignar una cookie.
- Cookie: capçalera que es llegeix des de la petició del navegador amb els valors de les cookies que hi ha emmagatzemades.
Per a esborrar una cookie, només cal enviar la cookie buida amb una data al camp “expires” antiga:
- Set-Cookie: nomgaleta=; expires=Thu, 01-Jan-1970 00:00:00 GMT;
Exemple GET/POST d’un formulari HTML: la URI /form mostra un formulari, que s’omple i processa la URI /submit.

Exemple de cookie: la URI /page1 emmagatzema una cookie, que després està disponible a altres pàgines.

Exemple de redirecció: la URI /page1 es redirecciona a /page2.

APIs sobre HTTP
Veurem dos tipus de protocols sobre HTTP: un stateless i un altre stateful.
RESTful API
REST (Representional State Transfer) és un estil d’arquitectura per a sistemes distribuïts. Permet establir comunicació d’aplicacions amb serveis proporcionats a la web. Per tal que una interfície es pugui anomenar RESTful, ha de cumplir una sèrie de principis:
- Ha d’implementar un esquema client/servidor. Això permet desenvolupar-los de forma independent, i reemplaçar-los.
- Ha de ser stateless (sense estat en el servidor). Per tant, l’estat s’ha de conservar al client. Això millora l’escalabilitat, la disponibilitat i el rendiment de l’aplicació.
- S’ha de donar informació al client (de forma implícita o explícita) de si el contingut és cacheable. Així, es pot millorar l’escalabilitat i rendiment.
- Ha de tenir una interfície uniforme. Bàsicament, un recurs ha de associar-se amb una URI que permeti accedir a les seves dades.
- Ha de dissenyar-se com a un sistema per capes. El client no pot saber específicament l’arquitectura del servei o on es troben les dades, per exemple.
- Opcionalment, el client pot demanar codi al servidor, per simplificar la seva implementació (poc habitual).
Encara que no és obligatori, un servei RESTful sovint utilitza HTTP com a protocol. En aquest cas, els cos de les peticions i les respostes solen tenir el format XML o bé JSON.
Si ens fixem en les operacions CRUD habituals, hi ha una convenció de com utilitzar els mètodes HTTP utilitzant els codis d’estat 200, 201, 204, 400, 404:
- GET: llegir (idempotent).
- POST: crear (no cacheable).
- PUT: actualitzar/reemplaçar.
- DELETE: esborrar.
- PATCH: modificació parcial.
Com que el protocol és sense estat (stateless), la autenticació/autorització ha de produir-se per cada petició. Les pràctiques recomanades inclouen utilitzar canals segurs, i no exposar mai dades a la URL. També es recomana l’ús d’Oauth.
L’ús de tokens, o paraules d’accés, és habitual als sistemes d’autenticació. El funcionament amb token és el següent:
- L’usuari o aplicació client accedeix al servei d’autenticació.
- Si és correcta, el servidor genera un token que envia al client.
- L’usuari accedeix als recursos amb el seu token.
Streaming API
Un protocol de tipus streaming és justament una inversió del RESTful. No es tracta d’una conversació. Es tracta d’obrir una connexió entre un client i l’API, on el client va rebent els nou resultats quan es produeixen, en temps real.
La seva naturalesa és stateful, ja que l’API envia els resultats en funció del perfil del client i/o de les regles de filtratge que hagi establert.
És habitual utilitzar el format JSON. En aquest cas, s’utilitza el format text i es poden delimitar els missatges amb fi de línia.
Un exemple és el de Twitter.
Criptografia
Referències
- Java Security Standard Algorithm Names, JDK 11
- Cryptographic Storage Cheat Sheet
- Security Developer’s Guide, JDK 11 Providers Documentation
- Block cipher mode of operation
- Java Criptography (Jenkov)
- Cryptographic hash function (Wikipedia)
- Public-key cryptography (Wikipedia)
- Java Security 2nd Edition (examples)
- Practical Cryptography for Developers
- Establishing a TLS Connection
- Generating and Verifying Signatures (The Java Tutorials)
- A Deep Dive on End-to-End Encryption: How Do Public Key Encryption Systems Work?
- Java Keytool - Commands and Tutorials
- Java KeyStores - the gory details
- Trusted Timestamping (Wikipedia)
- CyberChef
- Capture The Flag 101
- SideKEK
Conceptes
- Estats de les dades
- Xifrat
- Intercanvi de claus
- Resums de missatge
- Signatures digitals
- Autenticació
- Certificats
- Comunicació segura TLS
Principi de Kerckhoffs: “L’efectivitat d’un sistema criptogràfic no ha de dependre que el seu disseny o algorisme romangui en secret, sinó que ha de basar-se exclusivament en el secret de la clau.”
Això implica que la seguretat d’un sistema depèn íntegrament de la protecció de la clau privada. Si aquesta clau es compromet, el sistema queda vulnerable independentment de la robustesa de l’algorisme.
Estats de les dades
En un sistema segur, quan una dada s’ha d’utilitzar (en ús) cal que sigui en pla. Però si està emmagatzemada o transmetent-se, cal mantenir-la secreta. Aquests són els estats:
- En repòs: quan estan emmagatzemades digitalment en un dispositiu.
- En trànsit: quan s’estan movent entre dispositius o punts de les xarxa.
- En ús: quan una aplicació les està utilitzant, sense protecció.
Quan protegim les dades en repòs, preparem al nostre sistema per a l’eventualitat que es puguin llegir després d’un atac a la màquina on són. Quan protegim les dades en trànsit, ho fem perquè sabem que qualsevol pot escoltar el tràfic d’una xarxa.
Si s’exposen les nostres dades xifrades, i aconsegueixen accedir a les claus secretes associades (ja sigui al moment o més endavant), podran veure-les en pla.
De vegades podem evitar haver-les de guardar. Per exemple, si les podem demanar cada cop (p. ex. clau mestra), o bé comparar-les amb funcions hash (p. ex. contrasenyes).
Xifrat
La criptografia (del grec “kryptos” - amagat, secret - i “graphin” - escriptura. Per tant seria “escriptura oculta”) és l’estudi de formes de convertir informació des de la seva forma original cap a un codi incomprensible, de forma que sigui incomprensible pels que no coneguin aquesta tècnica.
En la terminologia de criptografia, trobem els següents elements:
- La informació original que ha de protegir-se i que es denomina text en clar o text pla.
- El xifrat és el procés de convertir el text pla en un text il·legible, anomenat text xifrat o criptograma.
- Les claus són la base de la criptografia, i són cadenes de números amb propietats matemàtiques.
Existeixen dos grans grups d’algorismes de xifrat, en funció de si les claus són úniques o van en parella:
- Simètrics: els algorismes que utilitzen una única clau privada per xifrar la informació i la mateixa per desxifrar-la. L’algorisme simètric més utilitzat és AES (Advanced Encryption Standard), amb claus de 128, 192 o 256 bits.
- Asimètrics: els que tenen dues claus, una pública i una altra privada, que permet xifrar amb una qualsevol i desxifrar amb l’altra. S’utilitzen principalment per a dos propòsits:
- Xifrat amb clau pública: un missatge xifrat amb clau pública només es pot desxifrar amb la clau privada.
- Signatura digital: un missatge signat amb la clau privada pot ser verificat per qualsevol amb la clau pública.
La clau simètrica té inconvenients. Primer, necessites una clau per cada parella origen/destí. Segon, necessites una forma segura de compartir-les. La clau asimètrica té l’avantatge que pots compartir la part pública de la clau, ja que sense la part privada no pots fer res, però els algorismes són més lents i tenen limitacions en la mida del missatge a xifrar. Per exemple, RSA té un missatge màxim de floor(n/8)-11 bytes, on n és la mida de la clau en bits (p. ex. 2048).
El xifrat també pot ser de bloc o de stream:
- Bloc: xifrat de blocs de mida fixa de dades. Poden ser asimètrics o simètrics.
- Stream: xifrat d’un stream (corrent) de bits o bytes. Són simètrics.
Si no es diu el contrari, parlem només de xifrats en bloc.
Xifrat simètric: les dues parts comparteixen una clau privada

Xifrat asimètric: només el propietari de les claus (el receptor) pot desxifrar les dades

Intercanvi de claus
Com ja hem vist, el xifratge simètric té el problema de compartir la clau entre les dues parts. I l’asimètric, no permet xifrar blocs gaire grans.
La solució és utilitzar els dos tipus de xifrat de forma combinada.
- La criptografia asimètrica ens permet compartir una clau privada de forma segura.
- La criptografia simètrica ens permet xifrar missatges més llargs i més ràpidament.
L’intercanvi de clau més senzill es pot fer amb xifrat de clau pública RSA: una part xifra el secret compartit amb la clau pública de l’altra. El problema és que aquesta acció no proporciona Perfect Forward Secrecy (PFS): si algú obté la clau privada en el futur, i ha guardat la conversa xifrada, podria desxifrar-la.

No obstant, l’algorisme més comú per fer intercanvi de clau és el Diffie-Hellman (DH), present en el preàmbul de la majoria de les comunicacions amb xifrat simètric. DH proporciona PFS perquè les claus es generen de forma efímera per a cada sessió i es descarten després.

Per a dues parts A i B, el procés és el següent:
- A i B generen dues parelles de claus pública/privada, Apub/Apri i Bpub/Bpri.
- Les dues parts s’intercanvien les claus públiques Apub i Bpub.
- En privat, cada part combina les claus públiques rebudes amb les privades (Apub+Bpri, Bpub+Apri). La característica essencial de DH és que aquesta combinació genera el mateix secret!
Resums de missatge
Un “message digest” o hash és una seqüència de bytes produïda quan un conjunt de dades passen per una funció hash. No és sempre necessària una clau perquè aquesta funció operi. Alguns algorismes coneguts: MD5, SHA-256. Les seves propietats són:
- És determinista (el mateix resultat per la mateixa entrada).
- És ràpida.
- La funció inversa no és viable.
- Un petit canvi d’entrada provoca un gran canvi de sortida.
- Dues entrades diferents no poden tenir el mateix hash.
Un resum ens permet protegir la integritat d’un missatge.
Veiem com funciona SHA-256 a Linux sobre un petit text “hello world!”. Si ho proveu, veureu que el resultat és instantani (2). Com que té 256 bits, genera un resum de 32 bytes (en hex). Mireu com, canviar un caràcter, canvia totalment el hash (4).
$ echo 'hello world!' | openssl sha256
(stdin)= ecf701f727d9e2d77c4aa49ac6fbbcc997278aca010bddeeb961c10cf54d435a
$ echo 'hello,world!' | openssl sha256
(stdin)= 4c4b3456b6fb52e6422fc2d1b4b35da2afbb4f44d737bb5fc98be6db7962073f
Si busqueu el resum de “hello world!” o el de “123456” el trobareu a la xarxa. Dues conclusions: per a un algorisme i una entrada, tenim la mateixa sortida (1). No tenim la funció inversa (3), però hi ha rainbow tables (taules precalculades de resums) que fan la correspondència entre textos comuns i els seus resums, i que s’utilitzen per esbrinar credencials.
Si volem protegir la integritat i l’autenticitat, podem utilitzar els MAC (message authentication code). Bàsicament, es tracta de resums segurs xifrats amb una clau privada que cal compartir entre les dues parts per tal de verificar la comunicació.

També tenim les Key Derivation Functions (KDF), una hash que permet derivar un o més secrets a partir d’un altre més petit, com un password. Les KDF permeten estendre claus (key stretching) en altres més llargues. A més, les KDF afegeixen un salt (valor aleatori únic per cada entrada) per evitar atacs amb rainbow tables, ja que el mateix password amb diferent salt genera resums completament diferents. Exemples de KDF: bcrypt, scrypt, Argon2.
En el següent diagrama es poden veure dos usos de les KDF:
- Per a guardar un hash d’una contrasenya, i poder comprovar si s’introdueix correctament.
- Per a generar claus d’algorismes simètrics a partir d’una contrasenya.

Signatures digitals
La signatura digital és un mecanisme de xifrat per autentificar informació digital. El mecanisme utilitzat és la criptografia de clau pública. Per això aquest tipus de signatura també rep el nom de signatura digital de clau pública. S’utilitzen per a garantir tres aspectes: autenticitat, integritat i no repudi. Juntament amb el xifrat, que garanteix la confidencialitat, aquests són els objectius fonamentals de la seguretat de la informació.
Aquest és el procés per a obtenir una signatura digital:
- Es calcula un resum de missatge per a les dades d’entrada.
- El resum es xifra amb la clau privada.
Aquest és el procés per a verificar una signatura digital:
- Es calcula un resum de missatge per a les dades d’entrada.
- El resum de la signatura digital es desxifra amb la clau pública.
- Es comparen els dos resums. Si són iguals, la signatura és correcta.
Signatura digital: el propietari de les claus (l’emissor) envia la prova de les dades originals

Autenticació
L’autenticació és el procés de confirmar l’autenticitat reclamada per una entitat. Tenim bàsicament dos tipus:
- Autenticació de dades: les dades no han estat modificades. Es pot aconseguir amb un algorisme de resum de missatge (message digest).
- Autenticació d’autor: l’autor és qui diu ser. Es pot aconseguir amb un protocol de desafiament-resposta (challenge-response).
L’autenticació no vol dir que tinguem xifratge.
Autenticació de dades
Prova que les dades no han estat alterades. Com hem vist a les seccions anteriors, hi ha tres nivells:
- Hash / resum de missatge: garanteix només la integritat (qualsevol modificació de les dades canvia el hash, però qualsevol persona pot calcular-lo, així que no prova qui l’ha generat).
- MAC / HMAC: garanteix integritat + autenticació (només qui té la clau secreta compartida pot generar un MAC vàlid, però com que la clau és compartida, qualsevol de les dues parts podria haver-lo creat, i per tant no hi ha no repudi).
- Signatura digital: garanteix integritat + autenticació + no repudi (només el propietari de la clau privada pot signar, i com que ningú més la té, no pot negar haver-ho fet).
Autenticació d’autor
Prova que l’altra part és qui diu ser. L’autenticació es basa en tres factors clàssics: alguna cosa que saps (contrasenya), alguna cosa que tens (certificat, smart card, token) i alguna cosa que ets (biometria). L’autenticació multifactor (MFA) combina dos o més d’aquests factors.
En l’àmbit criptogràfic, el mecanisme principal és el protocol de desafiament-resposta (challenge-response), basat en el factor “alguna cosa que tens” (la clau privada). Prova que l’altra part posseeix aquesta clau sense revelar-la:
- El verificador envia un desafiament (un valor aleatori o nonce) a l’entitat que vol autenticar.
- L’entitat respon amb una prova que demostra que posseeix la clau privada.
Hi ha dues variants:
- Desxifrar un desafiament: el verificador xifra el nonce amb la clau pública de l’entitat. L’entitat demostra la seva identitat desxifrant-lo amb la seva clau privada.
- Signar un desafiament: l’entitat signa el nonce amb la seva clau privada. El verificador comprova la signatura amb la clau pública.
Certificats
Si se signa un document digital utilitzant una clau privada, el receptor ha de tenir la clau pública per verificar la signatura. El problema és que una clau no indica a qui pertany. Els certificats resolen aquest problema: una entitat ben coneguda (Certificate Authority: CA) verifica la propietat de la clau pública que se t’ha enviat.
Un certificat conté:
- El nom de l’entitat per qui s’ha emès el certificat.
- La clau pública d’aquesta entitat.
- La signatura digital que verifica la informació en el certificat, feta amb la clau privada de l’emissor.
Un certificat podria contenir aquesta informació:
"L'autoritat de certificació 2276172 certifica que la clau pública de John Doe és 217126371812".
Problema: per verificar un certificat, necessitem la clau pública de la CA que l’ha signat. Però, qui garanteix que aquesta CA és legítima? Una altra CA de nivell superior pot haver signat el seu certificat, formant una cadena de certificats. Al final de la cadena hi ha un certificat arrel (root certificate), que està autosignat — és a dir, signat per la mateixa CA que l’emet. Com que no hi ha cap autoritat superior que el validi, hem de decidir confiar-hi explícitament. Per això, els navegadors i plataformes com Java inclouen una llista preinstal·lada de CA arrel considerades confiables.
Els certificats s’utilitzen en molts contextos: servidors web (TLS/SSL), signatura de codi, correu electrònic (S/MIME), autenticació de clients, etc. El més comú és el certificat de servidor TLS/SSL, que permet al navegador verificar que es connecta al servidor legítim. El client utilitza l’algorisme de validació del camí de certificació:
- El subject (CN) del certificat coincideix amb el domini al qual es connecta.
- El certificat l’ha signat un CA confiable.
Comunicació segura TLS
El handshake TLS és l’exemple més comú de com tots els conceptes anteriors treballen junts: certificats per identificar el servidor, signatures digitals per verificar la confiança, i intercanvi de claus per establir un canal xifrat.
Handshake TLS 1.2 simplificat (intercanvi RSA):
- El navegador es connecta al servidor i rep el seu certificat (que conté la clau pública del servidor).
- El navegador verifica el certificat: comprova que la signatura prové d’una CA confiable i que el domini coincideix.
- El navegador genera un secret aleatori, el xifra amb la clau pública del servidor i l’envia. Només el servidor pot desxifrar-lo (xifrat asimètric). Això és un desafiament-resposta per desxifrat: el servidor demostra la seva identitat perquè és l’únic que pot desxifrar el secret.
- Ambdues parts utilitzen el secret compartit com a llavor per derivar les claus de sessió (simètriques) mitjançant una funció de derivació de claus (KDF).
- A partir d’aquest moment, tota la comunicació utilitza xifrat simètric amb les claus compartides.
Handshake TLS 1.3 simplificat (ECDHE):
En TLS 1.3, l’intercanvi de clau RSA s’elimina i s’utilitza exclusivament Diffie-Hellman efímer (ECDHE), garantint Perfect Forward Secrecy. Però com que DH no autentica les parts, el servidor ha de demostrar la seva identitat d’una altra manera:
- Client i servidor fan un intercanvi de claus Diffie-Hellman per generar un secret compartit.
- El servidor signa la seva clau pública DH amb la seva clau privada del certificat, provant que els valors de l’intercanvi realment provenen d’ell i no d’un atacant. Això és un desafiament-resposta per signatura: el client verifica la signatura amb la clau pública del certificat.
- Tota la comunicació utilitza xifrat simètric amb les claus derivades del secret compartit.
Criptografia a Java (JCA)
Els motors criptogràfics de Java proporcionen mecanismes per signatures digitals, resums de missatges, etc. Aquests motors estan implementats per proveïdors de seguretat (java.security.Provider), que es poden visualitzar mitjançant java.security.Security.getProviders(). Cada motor (java.security.Provider.Service) té un tipus i un algorisme.
Claus
Podem generar claus de tipus simètric (KeyGenerator) o asimètric (KeyPairGenerator).
KeyGenerator keyGen = KeyGenerator.getInstance(algorithm);
keyGen.init(size);
SecretKey secretKey = keyGen.generateKey();
Algorismes simètrics típics són DES (56 bits) o AES (128, 192, 256 bits).
KeyPairGenerator kpg = KeyPairGenerator.getInstance(algorithm);
kpg.initialize(size);
KeyPair kp = kpg.generateKeyPair();
PublicKey publicKey = kp.getPublic();
PrivateKey privateKey = kp.getPrivate();
Cal indicar l’algorisme. El més habitual és RSA (1024, 2048 bits).
Tant SecretKey, com PublicKey i PrivateKey, són subclasses de java.security.Key. Totes elles tenen un mètode getEncoded(): la clau en format binari.
Xifrat
Per a poder xifrar, necessitem un objecte javax.crypto.Cipher:
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
El format del paràmetre (transformation) és algorithm/mode/padding. Mode i padding són opcionals: si no s’indiquen, s’utilitza el mode i padding per defecte. Compte: el mode per defecte habitualment és ECB, que és insegur. Cal especificar sempre el mode explícitament (p. ex. CBC, CTR o GCM). Què són mode i padding?
- Padding: és una tècnica que consisteix a afegir dades de farciment al començament, mig o fi d’un missatge abans de ser xifrat. Això es fa perquè els algorismes estan dissenyats per tenir dades d’entrada d’una mida concreta.
- Mode: defineix com es relacionen els blocs d’entrada (en pla) amb els de sortida (xifrats). El més senzill és el mode ECB: cada bloc es xifra de forma independent, cosa que significa que blocs d’entrada idèntics produeixen blocs de sortida idèntics, exposant patrons en les dades. Altres modes, com CBC o CTR, encadenen o combinen els blocs de manera que entrades idèntics generen sortides diferents.
Després, hem d’inicialitzar l’objecte utilitzant el mode (Cipher.ENCRYPT_MODE o Cipher.DECRYPT_MODE) i la clau de xifrat:
cipher.init(Cipher.ENCRYPT_MODE, key); // xifrat
cipher.init(Cipher.DECRYPT_MODE, key); // desxifrat
Finalment, realitzem el xifrat o desxifrat:
byte[] bytesOriginal = textOriginal.getBytes("UTF-8"); // necessito bytes com a entrada
byte[] bytesXifrat = cipher.doFinal(bytesOriginal);
El desxifrat podria ser:
byte[] bytesDesxifrat = cipher.doFinal(bytesXifrat);
// alternativament, si el contingut a desxifrar és una part de l'array:
byte[] bytesDesxifrat = cipher.doFinal(bytesXifrat, inici, longitud);
Alguns modes de xifrat en bloc utilitzen el que s’anomena vector d’inicialització (IV). Es tracta d’un paràmetre aleatori per a l’algorisme de xifrat que fa més difícil relacionar els blocs en funció de les seves entrades. Normalment no cal que sigui secret, només que no es repeteixi amb la mateixa clau.
Per utilitzar aquests modes (p.ex. CBC), cal afegir un nou paràmetre quan s’inicialitza el Cipher. La mida de l’IV és habitualment la mateixa del bloc. Per AES és de 16 bytes (128 bits).
byte[] iv = new byte[16]; // 16 bytes = 128 bits per AES
SecureRandom random = new SecureRandom();
random.nextBytes(iv);
IvParameterSpec parameterSpec = new IvParameterSpec(iv);
cipher.init(Cipher.ENCRYPT_MODE, key, parameterSpec); // xifrat
cipher.init(Cipher.DECRYPT_MODE, key, parameterSpec); // desxifrat
Xifrat de streams
Quan cal xifrar grans quantitats de dades, no és pràctic carregar-les totes en memòria per cridar doFinal(). Per a aquestes situacions, podem processar les dades de forma incremental amb streams. Aquest xifrat és sempre simètric.
A Java, tenim les classes CipherInputStream i CipherOutputStream.
CipherInputStream i CipherOutputStream admeten un Cipher simètric de bloc, com per exemple AES/ECB/PKCS5Padding, o bé els modes de tipus feedback CFB8 or OFB8, (8 = blocs de 8 bits), com per exemple AES/CFB8/NoPadding. Els modes feedback necessiten vectors d’inicialització (IV).
Per exemple, si volem obrir un arxiu i xifrar-lo o desxifrar-lo, podem fer-ho així:
FileInputStream in = new FileInputStream(inputFilename);
FileOutputStream fileOut = new FileOutputStream(outputFilename);
CipherOutputStream out = new CipherOutputStream(fileOut, cipher);
Llavors, caldria copiar el stream in a out.
L’objecte cipher ha d’inicialitzar-se amb el mode que calgui, ENCRYPT_MODE o DECRYPT_MODE.
CipherInputStream es pot utilitzar de forma anàloga. En aquest cas, el stream de sortida podria ser un FileOutputStream:
FileInputStream fileIn = new FileInputStream(inputFilename);
CipherInputStream in = new CipherInputStream(fileIn, cipher);
FileOutputStream fileOut = new FileOutputStream(outputFilename);
Dades binàries en text
Les claus i la informació xifrada està en format binari. Si cal intercanviar-ho utilitzant un canal de text, es poden convertir utilitzant Base64. A Java, tenim java.util.Base64.
byte[] binary1 = ...;
String string = Base64.getEncoder().encodeToString(binary1);
byte[] binary2 = Base64.getDecoder().decode(string);
// binary1 i binary2 són iguals
Resums, signatures i certificats
- Resums de missatges
- Resums segurs
- Key Derivation Functions
- Signatures digitals
- Desafiament-resposta (challenge-response)
- Certificats
Resums de missatges
Els resums s’implementen utilitzant la classe java.security.MessageDigest, que permet generar un resum de dades i protegir-ne la integritat. Si a més volem garantir l’autenticitat (que el missatge prové de qui diu), podem utilitzar javax.crypto.Mac, que genera un resum xifrat amb una clau secreta compartida. També es pot realitzar l’operació amb Streams gràcies a java.security.DigestInputStream i java.security.DigestOutputStream.
Alguns algorismes típics de resums: MD2, MD5, SHA-1, SHA-256, SHA-384, SHA-512.
MessageDigest messageDigest = MessageDigest.getInstance(algorithm);
byte[] resum = messageDigest.digest(text.getBytes());
Resums segurs
Un Message Authentication Code (javax.crypto.Mac) és un resum xifrat amb una clau secreta compartida. Només es pot verificar si tens aquesta clau. Podem generar-lo així:
Mac mac = Mac.getInstance(algorithm);
mac.init(key); // la clau secreta
byte[] macBytes = mac.doFinal(text.getBytes());
Un algorisme podria ser HmacSHA256 (HMAC: Hash-based MAC).
Key Derivation Functions
Amb una KDF, podem generar una clau més llarga que un password, per exemple. Aquí tenim un exemple utilitzant l’algorisme PBKDF2WithHmacSHA256.
SecretKeyFactory f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
KeySpec ks = new PBEKeySpec(password, salt, iterationCount, keyLength);
SecretKey rawSecret = f.generateSecret(ks); // secret genèric
SecretKey aesSecret = new SecretKeySpec(rawSecret.getEncoded(), "AES"); // secret AES
Els paràmetres del KeySpec són:
- password: un char[] amb la contrasenya.
- salt: un valor aleatori únic que s’afegeix al password per evitar que passwords idèntics generin el mateix hash (protecció contra rainbow tables).
- iterationCount: nombre d’iteracions per a generar el hash. Com més iteracions, més lent és el càlcul, dificultant atacs de força bruta.
- keyLength: longitud de la clau a generar.
Signatures digitals
Una signatura digital equival a fer un resum i xifrar-lo amb una clau privada. El receptor podria desxifrar-lo amb la pública, i comparar-lo amb un resum que faci de les dades (en pla) rebudes.
Els algorismes són variats, per exemple, SHA256withRSA indica que el resum es fa amb SHA256 i el xifratge amb RSA. Per tant, les claus utilitzades han de ser RSA. Per a signar una entrada (array de bytes):
Signature sign = Signature.getInstance(algorithm);
sign.initSign(privateKey);
sign.update(input);
byte[] signatura = sign.sign();
Per verificar-la:
Signature sign = Signature.getInstance(algorithm);
sign.initVerify(publicKey);
sign.update(input);
boolean correcte = sign.verify(signatura);
Desafiament-resposta (challenge-response)
El protocol de desafiament-resposta permet autenticar una entitat provant que posseeix una clau privada. El verificador genera un valor aleatori (nonce) i l’entitat respon demostrant que té la clau.
Variant 1: Desxifrar un desafiament
El verificador xifra el nonce amb la clau pública de l’entitat. L’entitat demostra la seva identitat desxifrant-lo.
1. El verificador genera un nonce aleatori i el xifra amb la clau pública de l’entitat:
byte[] nonce = new byte[32];
new SecureRandom().nextBytes(nonce);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] desafiament = cipher.doFinal(nonce);
2. El verificador envia desafiament (el nonce xifrat) → a l’entitat.
3. L’entitat desxifra el desafiament amb la seva clau privada:
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] resposta = cipher.doFinal(desafiament);
4. L’entitat retorna resposta (el nonce desxifrat) → al verificador.
5. El verificador compara nonce amb resposta. Si són iguals, l’entitat ha demostrat que posseeix la clau privada.
Variant 2: Signar un desafiament
El verificador envia el nonce en pla. L’entitat el signa amb la seva clau privada.
1. El verificador genera un nonce aleatori:
byte[] nonce = new byte[32];
new SecureRandom().nextBytes(nonce);
2. El verificador envia nonce (en pla) → a l’entitat.
3. L’entitat signa el nonce amb la seva clau privada:
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initSign(privateKey);
sign.update(nonce);
byte[] resposta = sign.sign();
4. L’entitat retorna resposta (la signatura) → al verificador.
5. El verificador verifica la signatura amb la clau pública de l’entitat:
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initVerify(publicKey);
sign.update(nonce);
boolean autenticat = sign.verify(resposta);
Certificats
Els certificats (java.security.cert.Certificate) més habituals són de tipus X.509, i indiquen una vinculació d’una identitat a una clau pública, garantida per una altra entitat autoritzada. Inclouen:
- data d’inici i fi
- versió (3 actualment)
- número de sèrie (únic per proveïdor)
- el DN (distinguished name) de la CA emissora
- el DN del subjecte del certificat
- la clau pública del subjecte
- la signatura digital de la CA que garanteix el certificat
Els DN (Distinguished Names) contenen una sèrie de camps (CN, OU, O, L, S, C) que identifiquen tant l’emissor com el subjecte.
Habitualment, els trobem dins dels magatzems. Per gestionar-los podem utilitzar l’eina keytool del JRE o bé programàticament amb la classe java.security.KeyStore i els mètodes getKey(alias) (clau privada) i getCertificate(alias) (certificat on hi ha la clau pública).
Són necessaris als portals web amb seguretat habilitada HTTPS.
Gestió de claus
Generació i destrucció
Consells sobre generació:
- Per a xifrat simètric, utilitzar AES amb almenys 128 bits, millor 256.
- Utilitzar modes autenticats si és possible: GCM, CCM. Aquests modes proporcionen xifrat autenticat, combinant confidencialitat i integritat en una sola operació (sense necessitat d’un MAC separat). Si no, CTR o CBC. ECB hauria d’evitar-se.
- Per a xifrat asimètric, utilitzar criptografia de corba el·líptica (ECC) com Curve25519. Si no, utilitzar RSA d’almenys 2048 bits.
- Utilitzar generadors aleatoris segurs. Millor CSPRNG (Cryptographically Secure Pseudo-Random Number Generator) que PRNG (Pseudo-Random Number Generator). A Java, preferir sempre
SecureRandomsobreRandom.
És important fer que les claus tinguin una durada limitada (rotar-les): generar una nova clau, desxifrar les dades amb l’antiga, rexifrar-les amb la nova, i destruir l’antiga de forma segura. Cal fer rotació de les claus si:
- Si se sap (o se sospita) que la clau anterior ha estat compromesa.
- Quan passi un cert temps predeterminat.
- Quan s’hagi xifrat una certa quantitat de dades.
- Si ha canviat la seguretat associada a un algorisme (nous atacs).
Emmagatzematge
Les aplicacions gestionen diferents tipus de secrets: credencials, claus de xifratge, claus privades de certificats, claus d’API, dades sensibles, etc.
Els secrets s’han de xifrar en repòs i en trànsit. Les claus per xifrar secrets s’anomenen Data Encryption Keys (DEK). Aquestes claus també s’han de protegir, i el que es fa és xifrar-les utilitzant el que es diu Key Encryption Key (KEK) o “Master Key”. Aquest esquema permet modificar la KEK mantenint les DEK, i per tant, sense requerir el rexifratge de les dades.
Consells amb les claus:
- Si és possible, no emmagatzemar mai la KEK. Per exemple, demanar-la interactivament quan calgui utilitzant una Key Derivation Function (KDF).
- Si és factible, utilitzar un HSM (Hardware Security Module).
- Millor no mantenir-les en memòria i en pla.
- Mai guardar-les al codi ni al git.
- Les DEK i les KEK s’han d’emmagatzemar en llocs diferents. Per exemple, la base de dades i el sistema d’arxius, o en màquines diferents.
- Si van a un fitxer, protegir els arxius amb permisos restrictius.
- Fer key stretching des d’una contrasenya per generar la KEK. Per exemple, amb la funció de derivació de claus PBKDF2.
El següent diagrama mostra una KEK/DEK amb vectors d’inicialització (KIV/DIV) i gestionades mitjançant una KDF, que permet generar la KEK a partir d’un password.

KeyStores
Un magatzem de claus, o keystore, és protegit per una contrasenya i pot contenir múltiples entrades identificades per un alias. Cada entrada pot ser:
- Claus: pot ser una clau (asimètrica o simètrica). Si es tracta d’una asimètrica, pot contenir una cadena de certificats.
- Certificats: una clau pública, habitualment l’arrel de CAs de confiança.
Les keystores poden tenir diferents formats (JKS, JCEKS, PKCS12, PKCS11, DKS). Els més utilitzats són:
- JKS (Java Key Store): format propietari de Java. Històricament, el més utilitzat. Extensió jks.
- PKCS#12: format standard. Format recomanat. Extensió p12 o pfx.
Java té una eina anomenada keytool per gestionar magatzems de claus. A continuació es mostren algunes comandes habituals.
Comanda per llistar els continguts d’una keystore:
keytool -list -v -keystore keystore.jks
Comanda per generar un keystore amb una parella de claus:
keytool -genkey -alias mydomain -keyalg RSA -keystore keystore.jks -keysize 2048
Comanda per exportar un certificat d’una keystore:
keytool -export -alias mydomain -file mydomain.crt -keystore keystore.jks
Comanda per importar un certificat a una keystore:
keytool -importcert -file mydomain.crt -keystore keystore.jks -alias mydomain
Certificats digitals
Un certificat digital és un document electrònic que actua com una mena d’identitat digital per a persones, serveis o dispositius. La seva funció principal és vincular una clau pública amb una identitat concreta de manera fiable, gràcies a la signatura digital d’una Autoritat de Certificació (CA).

Les CAs no signen directament tots els certificats digitals, sinó que utilitzen els certificats intermedis, que serveixen de pont entre els certificats arrel (CA) i els finals.
Aquestes són funcions que permeten els certificats:
- Autenticació: Verifiquen la identitat de l’entitat amb qui es comunica el sistema. Per exemple, quan accedeixes a un lloc web segur (HTTPS), el certificat del servidor t’assegura que estàs parlant amb el lloc web legítim.
- Integritat: Gràcies a la signatura digital, qualsevol modificació del certificat un cop emès pot ser detectada.
- Xifratge: Faciliten l’intercanvi segur de claus per establir connexions xifrades (com en TLS/SSL).
- No repudiació: Proporcionen una evidència que l’entitat posseïa la clau privada corresponent en el moment de la signatura.
L’especificació més comuna dels certificats és la X.509, que inclou aquests camps:
- Identificació del subjecte: El nom o l’entitat a la qual es fa referència (p. ex., domini web, nom de persona o organització).
- Clau pública: La clau que s’utilitza per xifrar dades o verificar signatures.
- Identificació de l’emissor (CA): Informació sobre l’autoritat de certificació que ha emès el certificat.
- Període de validesa: La data d’inici i de caducitat del certificat.
- Número de sèrie: Un identificador únic per al certificat.
- Algoritme de signatura: L’algoritme utilitzat per generar la signatura digital.
Els certificats X.509 segueixen un estàndard que defineix què han de contenir i com s’organitzen, però per a poder ser emmagatzemats, transmesos o usats en diferents entorns, cal “encapsular-los” en un format concret. Aquí tenim les principals maneres de codificar i empaquetar-los:
-
DER (Distinguished Encoding Rules): és una codificació binària per als certificats X.509. Extensions habituals:
.der,.cer,.crt. Aquest format és compacte i està dissenyat perquè l’estructura del certificat es pugui interpretar de manera inequívoca per les màquines. -
PEM (Privacy-Enhanced Mail): és essencialment la codificació DER convertida a text mitjançant Base64. Extensions habituals:
.pem,.crt,.cer. Els certificats en format PEM tenen delimitadors clars, com ara:
-----BEGIN CERTIFICATE-----
(dades codificades en Base64)
-----END CERTIFICATE-----
Aquest format és molt popular perquè és fàcil d’utilitzar en entorns de text i és compatible amb molts programes.
-
PKCS#7: és un format que permet empaquetar un o diversos certificats (per exemple, tot el camí de certificació) en un sol fitxer. Aquest format és molt utilitzat en entorns on es necessita transmetre tota la cadena de certificació, com en certs sistemes de correu electrònic segur (S/MIME).
-
PKCS#12: aquest format permet empaquetar no només els certificats X.509, sinó també la seva clau privada associada. Els fitxers PKCS#12 (habitualment amb extensions .pfx o .p12) solen estar protegits amb una contrasenya per garantir la seguretat de la clau privada.
Seguretat
Parlarem de la seguretat lògica (software) i activa (preventiva) associats al desenvolupament de programari.
Un sistema es pot considerar segur si ens cuidem dels següents aspectes, de més a menys significatius:
- Disponibilitat: els usuaris poden accedir a la informació quan ho necessiten.
- Confidencialitat: la informació és accessible només per aquells autoritzats a tenir accés.
- Integritat: mantenir les dades lliures de modificacions no autoritzades.
- Autenticació: verificació de la identitat.
- No repudi: ni l’emissor ni el receptor poden negar ser part en la comunicació que es produeix.
Dins de la programació, i en referència a la seguretat, parlarem dels següents aspectes:
- Disseny segur de programari per evitar vulnerabilitats.
- Control d’accés: registre, autenticació i autorització d’usuaris.
- Tecnologies, protocols i pràctiques que permeten construir sistemes segurs.
Referències
Seguretat:
- Security Features in Java SE (The Java Tutorials)
- Secure Coding Guidelines for Java SE
- Security by design (Wikipedia)
- Application Security (Wikipedia)
- SEI CERT Oracle Coding Standard for Java
- Top 10 Secure Coding Practices
- Secure Programming for Linux and Unix (Java specific)
- How to Learn Penetration Testing: A Beginners Tutorial
- OWASP Proactive Controls
- OWASP API Security Project
- Role-based access control (Wikipedia)
- Please, stop using local storage
- HTTP headers for the responsible developer
- How to Use Local Storage with JavaScript
- Advanced API Security (Llibre)
- How secure is Java compared to other languages?
- The Rule Of 2
- Programming With Assertions
Autenticació / autorització:
- HTTP Authentication
- Session vs Token Based Authentication
- The Web Authentication Guide Cheatsheet
- JWT, JWS and JWE for not so dummies!
- Java Authentication with JSON Web Tokens (jjwt)
- Tutorial: Create and Verify JWTs in Java
- Refresh Tokens: When to Use Them and How They Interact with JWTs
- Attacking JWT authentication
- Token based authentication made easy
- Getting Token Authentication Right in a Stateless Single Page Application
- Stateless Sessions for Stateful Minds: JWTs Explained and How You Can Make The Switch
- Web Security for SPAs
- Webauthn guide
- An Introduction to OAuth 2
- OAuth 2.0 clients in Java programming, Part 1, Part 2 i Part 3
- LDAP Security
- REST Security Cheat Sheet (OWASP)
- Password Storage Cheat Sheet (OWASP)
- Cryptographic Storage Cheat Sheet (OWASP)
- REST API Security Essentials
Disseny segur
- Criteris de disseny
- Pràctiques de codificació
- Verificació del codi
- OWASP Top 10
- Llista de verificació de seguretat
La seguretat ha de ser una preocupació, no una funcionalitat. Afegir-la a posteriori és costós i poc efectiu: les decisions de disseny preses al principi determinen la superfície d’atac de tot el sistema.
Aquest document s’organitza en cinc seccions progressives. Els criteris de disseny estableixen els principis fonamentals que han de guiar qualsevol decisió arquitectònica. Les pràctiques de codificació tradueixen aquests principis en hàbits concrets. La verificació del codi presenta les eines per detectar problemes. La taula OWASP Top 10 connecta riscos reals amb els controls que els prevenen. Finalment, la llista de verificació proporciona un checklist pràctic per auditar el codi abans de posar-lo en producció.
Els documents següents apliquen aquests principis en àrees específiques: Control d’accés desenvolupa el registre, l’autenticació i l’autorització d’usuaris, i Tecnologies, protocols i pràctiques cobreix les implementacions concretes de protocols i eines.
Criteris de disseny
Aquesta és una llista de possibles criteris a tenir en compte per a dissenyar codi segur:
- El menor privilegi: una entitat només ha de tenir el conjunt necessari de permisos per realitzar les accions per a les quals estiguin autoritzades, i cap altre més (per exemple, una connexió de base de dades que només necessita llegir no hauria de tenir permisos d’escriptura o eliminació).
- Fail-Safe per defecte: el nivell d’accés predeterminat d’un usuari a qualsevol recurs del sistema hauria de ser “denegat” a menys que se’ls concedís un “permís” explícitament (per exemple, un nou endpoint d’API hauria de requerir autenticació per defecte, no ser públic fins que algú recordi protegir-lo).
- Economia del mecanisme: el disseny ha de ser el més simple possible. Això els fa més fàcils d’inspeccionar i confiar (per exemple, una sola funció centralitzada de comprovació de permisos és més segura que verificacions disperses pel codi).
- Mediació completa: un sistema ha de validar els drets d’accés a tots i cadascun dels seus recursos (per exemple, comprovar permisos no només a la interfície sinó també a l’API i a la consulta SQL).
- Disseny obert: els sistemes s’han de construir de forma oberta, sense secrets ni algorismes confidencials (per exemple, la seguretat ha de dependre de les claus, no de mantenir l’algorisme en secret — principi de Kerckhoffs).
- Separació de privilegis: la concessió de permisos a una entitat ha de basar-se en múltiples condicions, no només una (per exemple, requerir MFA per a operacions crítiques, no només la contrasenya).
- Mecanisme menys comú: qualsevol cosa que es comparteixi entre diferents components pot ser una via de comunicació i un potencial forat de seguretat, i per tant s’han de compartir les dades mínimes possibles (per exemple, microserveis amb bases de dades separades en lloc d’una base de dades compartida).
- Acceptabilitat psicològica: els mecanismes de seguretat no haurien de fer més difícil l’accés al recurs que si no hi fossin (per exemple, passkeys que substitueixen contrasenyes oferint més seguretat amb menys fricció per a l’usuari).
- Responsabilitat: el sistema ha de registrar qui és responsable d’utilitzar un privilegi. Si hi ha abús, podrem identificar el responsable (per exemple, logs d’auditoria que registren qui ha accedit a dades sensibles i quan).
Pràctiques de codificació
Aspectes tècnics que potencien la seguretat del codi:
- Immutabilitat: ens podem estalviar problemes associats a la integritat i disponibilitat de les dades.
- Disseny fail-fast per contractes: establint clarament quines són les precondicions i postcondicions perquè quelcom funcioni correctament.
- Validació: validem l’origen, la mida, el context, la sintaxi i semàntica de les dades que interactuen amb el sistema. Existeixen llibreries de validació per a tots els llenguatges principals.
- Redueix l’acoblament i la superfície d’API exposada: utilitza els mecanismes de visibilitat del llenguatge per exposar només el mínim necessari.
- Evita mecanismes que eludeixen el sistema de tipus (serialització, reflection, introspection) tret que sigui estrictament necessari.
- No exposis credencials o informació personal al codi font o a arxius de recursos: utilitza variables d’entorn o un gestor de secrets.
- Utilitza llibreries conegudes i testades, segueix les vulnerabilitats de dependències de tercers i actualitza a l’última versió.
- Utilitza sempre prepared statements per a accés a bases de dades (evita SQL injection).
- No mostris informació de la implementació als missatges d’error.
- Controla que l’entrada al sistema no causi ús desproporcionat de CPU, memòria i espai de disc.
- Allibera els recursos sempre: fitxers, connexions, memòria, etc.
- Preveu overflows aritmètics en operacions amb enters: utilitza les funcions de comprovació que ofereixi el teu llenguatge o llibreria estàndard.
Verificació del codi
Tenim eines dinàmiques (que executen el codi) i estàtiques (analitzen el codi sense executar-lo).
Dinàmic — DAST (Dynamic Application Security Testing):
- Proves d’integració i unitàries: cal dissenyar proves que verifiquin el comportament del codi. Cada llenguatge té els seus frameworks de test (JUnit, pytest, Jest, etc.).
- Code coverage: eines que mesuren quin codi s’ha executat durant els tests i quins camins queden sense cobrir, ajudant a decidir on cal afegir test cases.
Estàtic — SAST (Static Application Security Testing):
- Static code analysis: analitza el codi font sense executar-lo, detectant errors semàntics, patrons insegurs i vulnerabilitats. Eines com Semgrep o CodeQL són polivalents; la majoria de llenguatges tenen també eines específiques (SpotBugs per Java, Bandit per Python, clippy per Rust, etc.).
Per a una visió més completa de com integrar aquestes eines en pipelines CI/CD, vegeu la secció CI/CD Security.
OWASP Top 10
L’OWASP publica periòdicament el Top 10 de riscos de seguretat web i el Top 10 de controls proactius que tot desenvolupador hauria de conèixer. Són dues cares de la mateixa moneda: cada risc té un control que el prevé. La taula següent els relaciona i apunta a les seccions d’aquest curs on es desenvolupen en detall.
| Risc (OWASP 2025) | Control | Detall |
|---|---|---|
| Pèrdua del control d’accés | Control d’accés granular, denegar per defecte | Autorització, IDOR |
| Configuració incorrecta | Configuració segura per defecte, headers de seguretat | Security Headers |
| Fallades en la cadena de subministrament | Auditoria de dependències, SBOM, verificació d’integritat | Dependency Security, CI/CD Security |
| Fallades criptogràfiques | Xifrar dades en trànsit i en repòs amb protocols establerts | TLS/HTTPS, Secrets Management |
| Injecció | Prepared statements, validació d’entrades | Injection Attacks, Input Validation |
| Disseny insegur | Integrar seguretat des del disseny | Criteris de disseny |
| Fallades d’autenticació | MFA, gestió segura de sessions | Autenticació, Factors combinats |
| Fallades d’integritat | Verificació de dependències i pipelines | Dependency Security, SRI, CI/CD Security |
| Logging i alertes insuficients | Centralització de logs, monitoratge i alertes | Logging i Monitoring |
| Gestió incorrecta de condicions excepcionals | Errors segurs, IDs de correlació, no exposar informació interna | Gestió Segura d’Errors |
Llista de verificació de seguretat
Aquesta llista és un punt de partida per a una revisió de seguretat abans de posar en producció una aplicació. No és exhaustiva, però cobreix els controls més crítics.
Autenticació i Gestió de Sessions
- Les contrasenyes s’emmagatzemen amb un algorisme KDF (bcrypt, Argon2) amb salt
- Les contrasenyes noves es comproven contra bases de dades de filtracions conegudes (HaveIBeenPwned)
- Els missatges d’error d’autenticació són genèrics (no diferencien usuari inexistent de contrasenya incorrecta)
- Les sessions es regeneren immediatament després del login
- Les cookies de sessió tenen els flags
HttpOnly,SecureiSameSite - Hi ha un idle timeout i un absolute timeout de sessió configurats
- El logout invalida la sessió al servidor (no només elimina la cookie al client)
- MFA disponible i recomanat per als usuaris
Control d’Accés i Autorització
- Totes les peticions comproven autorització al backend, no al frontend
- Les consultes a la base de dades filtren per l’usuari autenticat (prevenció d’IDOR)
- Els rols i permisos es comproven al backend en cada petició
- El principi de privilegi mínim s’aplica a rols d’usuari i credencials de base de dades
- Cap camp sensible (rol, preu, estat) és modificable directament pel client (prevenció de mass assignment)
Validació d’Entrades i Sortides
- Totes les entrades es validen per longitud, tipus i format (whitelist)
- S’utilitzen prepared statements o ORMs per a totes les consultes SQL
- Les sortides HTML s’escapen automàticament per prevenir XSS
- Els fitxers pujats es validen pel contingut real (magic bytes), es renombren amb un UUID i s’emmagatzemen fora del web root
Gestió d’Errors i Logging
- Els errors retornats a l’usuari no contenen stack traces ni informació interna
- S’utilitzen IDs de correlació per relacionar errors d’usuari amb logs del servidor
- Les pàgines d’error per defecte del framework estan desactivades en producció
- S’enregistren els intents d’autenticació fallits, canvis de permisos i accés a recursos sensibles
Comunicació i Headers
- HTTPS és obligatori i HSTS està activat
- Les capçaleres de seguretat estan configurades: CSP,
X-Content-Type-Options,X-Frame-Options,Referrer-Policy - CORS no permet origens genèrics (
*) quan hi ha credencials implicades - La protecció CSRF està activa (tokens CSRF o cookies
SameSite)
Dependències i Configuració
- Les dependències s’escanegen per vulnerabilitats en el CI/CD
- Cap credencial ni secret està hardcoded al codi font ni al repositori
- Els secrets es gestionen via variables d’entorn o un vault
APIs (si aplica)
- El rate limiting protegeix els endpoints d’autenticació i les operacions costoses
- Els errors d’API no exposen missatges interns de base de dades ni stack traces
- La introspection de GraphQL està desactivada en producció
Control d’accés
El control d’accés inclou les activitats de registre, autenticació i autorització d’usuaris. És una de les àrees on els errors de disseny tenen més impacte: un sistema amb criptografia excel·lent però amb una gestió de sessions deficient continua sent vulnerable.
Aquest document cobreix les tres fases seqüencialment: com emmagatzemar credencials de forma segura en el registre, com verificar la identitat de l’usuari en cada petició (incloent gestió de sessions, emmagatzematge d’identificadors i autenticació multifactor), i com determinar quins recursos pot accedir un cop autenticat. Les tecnologies i protocols concrets (OAuth, JWT, WebAuthn) es desenvolupen a Tecnologies, protocols i pràctiques.
Registre
El registre d’usuaris té associat l’emmagatzematge d’aquella informació necessària per poder autenticar-los posteriorment. És important evitar deixar la informació en clar en fitxers o bases de dades, per estalviar-nos problemes de seguretat. També, evitar encriptar els passwords.
Un esquema habitual és l’ús de resums o hash. Si guardem el hash a la BBDD, no sabrem quin és el password, però podem comparar el que entra l’usuari amb el hash guardat, i dir si és el mateix.
Això només té un problema: hi ha taules preconstruïdes per a cercar les correspondències entre hash i password. Això ens obliga a afegir un string random (salt) al costat del password, i llavors el hash de tot plegat no és sempre el mateix per al mateix password. Aquest salt no ha de ser privat, compleix l’objectiu de fer inútils les tàctiques habituals per esbrinar passwords, i per tant es pot guardar en clar a la BBDD.
A l’hora de fer l’autenticació només haurem de fer la comparació entre el hash emmagatzemat i el calculat:
hash(salt + password1) és igual a hash(salt + password2)?
Una tècnica per a generar resums més segurs és el key stretching, que fa que la seva generació sigui lenta per a fer més difícil un atac de força bruta. Els algorismes KDF (Key Derivation Function) són un exemple.
A l’hora de definir una política de contrasenyes, les recomanacions actuals (NIST SP 800-63B) s’allunyen de les regles de composició obligatòria (majúscules, símbols, números) perquè generen patrons predictables com Password1!. En canvi, s’emfatitza la longitud: un mínim pràctic de 12-15 caràcters és més efectiu que la complexitat. No s’ha d’imposar una longitud màxima que trunci contrasenyes, ja que això és un indicador de comparació en pla.
Un complement essencial és la detecció de filtracions: en el moment del registre i de cada canvi de contrasenya, cal comprovar si la nova contrasenya apareix en bases de dades de credencials filtrades (com HaveIBeenPwned). Això es fa amb el model de k-anonimitat: s’envia únicament els primers 5 caràcters del hash SHA-1 de la contrasenya a l’API, i es comprova localment si el sufix del hash apareix en la llista retornada. Així mai s’exposa la contrasenya ni el hash complet.
Respecte al bloqueig de compte, el bloqueig dur després de N intents fallits és en si mateix un vector de denegació de servei contra usuaris legítims. Les alternatives preferides són: retard exponencial entre intents, CAPTCHA a partir d’un cert llindar, limitació de taxa per IP i notificació a l’usuari. Si s’utilitza bloqueig, hauria de ser temporal (desbloqueig automàtic als 30 minuts) i no requerir intervenció manual.
Finalment, el restabliment de contrasenya és una porta d’entrada crítica: els tokens de restabliment han de ser d’un sol ús, de durada limitada (15-30 minuts), i enviats per un canal verificat (correu electrònic o SMS al número registrat).
Autenticació
L’autenticació implica, habitualment, recollir la identificació de l’usuari per tal de comprovar la seva autenticitat.
Un cop tenim l’usuari autenticat, aquest pot rebre un identificador generat pel servidor i que el client haurà de fer arribar cada petició al servidor per tal de confirmar que està autenticat.
Les aplicacions client / servidor es poden diferenciar en dos tipus: stateful i stateless: amb i sense estat. Això es refereix al fet que el servidor emmagatzemi o no dades associades a l’usuari autenticat, el que s’anomena sessió.
- Stateful: amb sessió i dades emmagatzemades al servidor. L’identificador generat és el de la sessió. El servidor passa un ID al client, que utilitza cada cop que es comunica amb el servidor. El servidor l’utilitza per obtenir les dades que té associades.
- Stateless: sense sessió i dades emmagatzemades al client. L’identificador generat es diu token. Pot ser simplement un ID aleatori generat, però habitualment contenen informació associada que ha estat signada criptogràficament.
Identificador al client
A les aplicacions web, si un client està autenticat cal que li faci saber al servidor mitjançant algun tipus d’identificador secret. El client podria ser un navegador, si és una aplicació web d’usuari, o una aplicació client.
Si el client és un navegador, hi ha bàsicament dos esquemes per guardar aquest identificador al client: cookies i web storage.
- Les cookies són part del protocol HTTP. Permeten guardar galetes nom/valor mitjançant una capçalera “Set-Cookie” des del servidor (resposta), i informen el servidor de les galetes actuals mitjançant una capçalera “Cookie” des del navegador.
- El web storage és un mecanisme activable des del client exclusivament, mitjançant scripting. Tenim dos objectes, sessionStorage i localStorage, que permeten accions del tipus setItem/getItem sobre parelles nom/valor. No és un mecanisme que directament substitueixi les cookies, tot i ser semblants.
Aquestes dues tecnologies podrien emmagatzemar identificacions per accedir a aplicacions. Les cookies envien la informació directament al servidor, mentre el web storage permet gestionar la informació al client, exclusivament.
Si es tracta d’una aplicació client, aquesta informació la pot guardar el programari corresponent, i enviar-la quan calgui al servidor.
En el cas que l’identificador no estigui xifrat, és important que no sigui fàcilment deduïble per evitar que es puguin construir maliciosament (ID aleatori i llarg). JWT proporciona l’opció de xifrar la informació d’accés i autorització.
Les implicacions de seguretat dels dos mecanismes són molt diferents. localStorage i sessionStorage són completament vulnerables a XSS: qualsevol script que s’executi a la pàgina pot llegir i modificar tot el contingut, exposant tokens d’accés o informació sensible. IndexedDB té les mateixes vulnerabilitats respecte a XSS.
Les cookies amb HttpOnly no són accessibles des de JavaScript, protegint contra XSS. La flag Secure assegura que només es transmetin per HTTPS. SameSite=Strict prevé CSRF. Això fa les cookies molt més segures per emmagatzemar tokens de sessió o refresh tokens.
La millor pràctica per a SPAs és mantenir tokens d’accés en memòria (variables JavaScript), mai en localStorage. El token es perd quan es tanca el tab, però això és acceptable si és de curta durada (minuts). Un refresh token de llarga durada es pot guardar en una httpOnly cookie. Quan el token d’accés expira, JavaScript fa una petició a un endpoint específic que llegeix la refresh cookie i retorna un nou access token.
Enviament de l’identificador
A continuació, es comenten alguns possibles mètodes per a enviar l’identificador al servidor.
-
HTTP Basic Authentication utilitza una capçalera del tipus:
Authorization: Basic base64(username:password) -
Les cookies són el mètode més clàssic, i permeten dos headers especials, un del servidor:
Set-Cookie: sessionId=shakiaNg0Leechiequaifuo6Hoochoh; path=/; Secure; HttpOnly; SameSitei un altre des del client:
Cookie: sessionId=shakiaNg0Leechiequaifuo6Hoochoh -
Els tokens (bearer) es passen utilitzant una capçalera:
Authorization: Bearer ujoomieHe2ZahC5bEls tokens solen tenir un límit de validesa, i s’utilitzen sovint amb aplicacions stateless.
-
Les firmes (signatures) signen i envien les dades significatives de la petició en format formulari. Per exemple: APIs de serveis cloud.
-
Els certificats de client TLS realitzen un handshake abans de cap petició HTTP.
Factors combinats
L’autenticació es pot fer a partir d’alguna cosa que l’usuari sap, té o és. Podem tenir un sol factor d’autenticació, o combinar-los. És habitual tenir un doble factor d’autenticació en serveis més segurs.
Un segon factor habitual és el One-Time Password (OTP). Es poden basar en sincronització de temps o algorismes matemàtics que generen cadenes. Hi ha dues implementacions: HOTP i TOTP. La diferència és què comparteixen per generar la contrasenya: un comptador o el temps.
- El servidor crea una clau secreta per a l’usuari, i la comparteix amb un codi QR sobre una sessió segura (HTTPS).
- Les dues parts generaran l’OTP utilitzant un secure hash (tipus HMAC), i el servidor haurà de validar si és l’esperat.
La generació es fa amb aquesta fòrmula:
- hash (shared secret + counter) = HOTP
- hash (shared secret + time) = TOTP
Aplicacions TOTP com FreeOTP o andOTP emmagatzemen el secret i generen codis localment, sense requerir connexió a internet. La validació al servidor ha d’acceptar codis del període actual i adjacent per compensar el clock skew.
SMS i email com a segon factor són còmodes però menys segurs. Els SMS són vulnerables a SIM swapping, on un atacant convenç l’operadora per transferir el número de la víctima a una SIM controlada per l’atacant. L’email és tan segur com el compte de correu, que sovint està protegit només per contrasenya. Malgrat això, són millor que res i més fàcils d’adoptar per usuaris no tècnics.
La biometria (empremtes, reconeixement facial) proporciona una experiència d’usuari excel·lent. És important entendre que els sistemes moderns no emmagatzemen imatges de la teva cara o empremta, sinó templates criptogràfics derivats. En dispositius moderns, aquestes dades romanen en un enclavament segur del hardware (TEE/TPM) i mai surten del dispositiu.
Els backup codes són essencials per a la recuperació d’accés. Són típicament 8-10 codis d’un sol ús que l’usuari ha de guardar de manera segura. Quan es queden sense dispositius MFA, poden utilitzar un d’aquests codis per accedir i reconfigurar l’autenticació.
Autenticació amb sessió
Seguretat de sessions
La seguretat de les sessions comença amb com es transmet i s’emmagatzema el ID. Les cookies de sessió han de tenir les flags Secure (només per HTTPS), HttpOnly (inaccessibles des de JavaScript, protegint contra XSS) i SameSite (controla quan s’envien en peticions cross-site).
SameSite=Strict és el més restrictiu: les cookies només s’envien en peticions same-site. Això protegeix fortament contra CSRF però pot trencar fluxos legítims, com seguir un enllaç d’email a la teva aplicació. SameSite=Lax és un bon compromís: les cookies s’envien en navegació top-level (GET) però no en peticions cross-site POST o XMLHttpRequest.
La fixació de sessió és un atac on un atacant estableix l’ID de sessió de la víctima a un valor conegut abans que s’autentiqui. Després del login, l’atacant pot utilitzar aquest ID per segrestar la sessió. La defensa és simple: regenerar l’ID de sessió immediatament després de qualsevol canvi en el nivell de privilegi, especialment després del login.
Els timeouts de sessió proporcionen dues proteccions diferents. L’idle timeout invalida sessions inactives, protegint contra l’ús d’una sessió abandonada. L’absolute timeout invalida sessions després d’un període màxim independentment de l’activitat, forçant reautenticació periòdica. Els valors apropiats depenen de la sensibilitat de l’aplicació.
Single Sign-On (SSO) permet als usuaris autenticar-se un cop i accedir a múltiples aplicacions. OAuth/OIDC són els protocols estàndard moderns per implementar SSO. Quan un usuari s’autentica al proveïdor d’identitat (IdP), cada aplicació pot obtenir els seus propis tokens sense requerir credencials addicionals. La gestió de Single Sign-Out, on tancar sessió en una aplicació tanca sessió en totes, requereix coordinació entre aplicacions i l’IdP.
L’error més comú és no regenerar l’ID de sessió immediatament després del login. Si el servidor reutilitza el mateix ID que tenia l’usuari abans d’autenticar-se, un atacant que conegués aquest ID pot accedir a la sessió ja autenticada (session fixation). La regeneració és una crida a una funció, però si s’oblida obre una porta crítica.
Autenticació amb token
Autorització
Un cop l’usuari ha estat autenticat, hi ha un nombre de permisos que se li assignen en funció del seu rol dins de l’aplicació. Hi ha diferents formes d’assignar-los:
- Nivell: els usuaris i les tasques tenen nivells, un usuari pot fer les tasques amb nivell igual o menor al seu.
- Usuari: es fan parelles usuari-tasca (many2many)
- Grup: un usuari té un grup, es fan parelles grup-tasca
- Responsabilitat: un usuari pot tenir diversos grups
Un cop assignats els permisos, és important fer-los efectius en cadascuna de les interaccions de l’usuari amb el sistema. Això pot fer-se tant stateless (exemple: autoritzacions dins de JWT) com stateful (emmagatzematge a la sessió del servidor).
La majoria de frameworks implementen el model RBAC (Role-Based Access Control): els permisos s’assignen a rols, i els rols als usuaris. Per a escenaris més granulars, s’utilitza ABAC (Attribute-Based Access Control), on la decisió depèn d’atributs de l’usuari, del recurs i del context (hora del dia, IP d’origen, etc.).
Un error comú és comprovar l’autorització únicament a la capa de presentació (la interfície d’usuari). L’autorització s’ha d’aplicar a totes les capes: al controlador o endpoint de l’API, i a nivell de consulta a la base de dades. En particular, cal verificar que l’usuari autenticat és propietari de l’objecte específic que sol·licita, no només que té el rol adequat — vegeu la secció IDOR i Falles de Lògica de Negoci per al patró concret d’implementació.
Tecnologies, protocols i pràctiques
- Introducció
- Autenticació i Autorització
- Seguretat Web
- Seguretat d’APIs
- Seguretat en el Desenvolupament Modern
- Seguretat en Aplicacions Mòbils
Introducció
Els criteris de disseny i els models de control d’accés vistos anteriorment necessiten suport tecnològic concret. Aquest document és una referència de les principals tecnologies, protocols i pràctiques que permeten implementar-los en sistemes reals.
Està organitzat en cinc àrees: autenticació i autorització, seguretat web, seguretat d’APIs, seguretat en el desenvolupament modern, i aplicacions mòbils. Cada secció pot llegir-se de forma independent com a consulta.
Autenticació i Autorització
OAuth i OpenID Connect
Imagina que la teva aplicació necessita accedir al calendari d’un usuari en un servei extern. L’alternativa ingènua seria demanar-li la contrasenya del servei — però llavors la teva aplicació tindria accés a tot el compte i l’usuari no podria revocar-lo sense canviar la contrasenya. OAuth resol aquest problema: permet que un usuari autoritzi la teva aplicació a accedir a un recurs concret en el seu nom, sense revelar les seves credencials.
El resultat és un token d’accés de vida limitada i scope limitat. Si es filtra, el dany és contenible: no és la contrasenya de l’usuari.
Hi ha dos fluxos principals segons el context:
-
Authorization Code Flow amb PKCE — per a qualsevol aplicació amb un usuari interactiu (web, mòbil, SPA). L’usuari és redirigit al proveïdor d’identitat (el teu servidor o un proveïdor extern), s’hi autentica, i el proveïdor retorna un codi de curta durada. L’aplicació intercanvia aquest codi per un token en una crida servidor a servidor. PKCE (Proof Key for Code Exchange) és una prova criptogràfica que impedeix que un codi interceptat pugui ser aprofitat per un tercer: el client genera un secret al principi del flux i ha de demostrar que el posseeix en fer l’intercanvi.
-
Client Credentials Flow — per a comunicació màquina a màquina, sense usuari interactiu. El servei s’autentica directament amb el proveïdor usant les seves pròpies credencials i obté un token. Típic en microserveis que es criden entre ells.
OpenID Connect (OIDC) és una capa d’identitat construïda sobre OAuth. La distinció clau: OAuth respon a “pots accedir a aquest recurs?”, OIDC respon a “qui ets?”. OIDC afegeix un ID Token — un JWT signat amb informació sobre l’usuari autenticat (nom, email, identificador). Aquest token és per al client, no per enviar-lo a APIs. És el mecanisme que hi ha darrere dels botons “Inicia sessió amb…” que veiem a tot arreu.
L’error més freqüent és implementar la lògica d’autenticació manualment. OAuth i OIDC tenen molts detalls subtils — validació del paràmetre state per evitar CSRF, verificació del nonce, comprovació del camp aud del token — que una implementació casolana fàcilment omet. Utilitza sempre una biblioteca establerta per al teu framework o un proveïdor d’identitat gestionat.
JSON Web Tokens
Un cop un usuari s’ha autenticat, el servidor necessita una manera de reconèixer-lo en les peticions següents sense demanar-li les credencials cada cop. Una opció és guardar l’estat al servidor (sessions); una altra és que el propi token porti la informació necessària per verificar-lo sense cap consulta externa. Els JSON Web Tokens (JWT) segueixen aquest segon enfocament: un JWT és un token autònom i signat que el servidor pot verificar criptogràficament sense accedir a cap base de dades.
Un JWT té tres parts separades per punts: header, payload i signature. El header especifica l’algoritme de signatura. El payload conté les claims (afirmacions): qui és l’usuari, quan expira el token, per a qui és vàlid. La signature assegura que ningú ha alterat el contingut. JWT és el format de token estàndard de facto: l’utilitzen els principals proveïdors d’identitat (Keycloak, etc.) i és el format que retornen els fluxos OAuth/OIDC descrits anteriorment.
Per a aplicacions on el client no pot mantenir secrets (SPAs, apps mòbils), s’han d’utilitzar algoritmes asimètrics com RS256 o ES256. El servidor signa amb la seva clau privada, i qualsevol pot verificar amb la clau pública. HS256, que utilitza una clau simètrica, només és apropiat per a comunicació servidor a servidor on ambdós extrems poden mantenir el secret de manera segura.
Les claims estàndard tenen significats específics: iss identifica qui ha emès el token, sub identifica el subjecte (usuari), aud especifica per a qui està destinat, exp marca quan expira, iat quan va ser emès i nbf abans de quina data no és vàlid. La validació estricta d’aquestes claims és fonamental per a la seguretat.
Una pràctica essencial és mantenir els tokens d’accés de curta durada, típicament entre 5 i 15 minuts. Això limita la finestra d’oportunitat si un token és compromès. Per evitar que l’usuari hagi de reautenticar-se constantment, s’utilitzen refresh tokens de llarga durada que permeten obtenir nous tokens d’accés. Els refresh tokens s’han de rotar cada vegada que s’utilitzen: quan el client utilitza un refresh token, rep un nou token d’accés i un nou refresh token, invalidant l’anterior. Si un refresh token robat s’utilitza, el servidor detecta la reutilització i pot revocar tota la cadena de tokens.
Per a l’emmagatzematge segur de tokens al navegador, vegeu Control d’accés — Identificador al client.
Passkeys i WebAuthn
Les contrasenyes tenen tres debilitats estructurals: es poden phishing (l’usuari les escriu en un lloc fals sense adonar-se’n), es poden robar de la base de dades del servidor si no estan ben protegides, i els usuaris les reutilitzen entre serveis. Per resoldre-ho hi ha tres conceptes relacionats però diferents:
- WebAuthn és l’API del navegador que permet autenticar-se amb criptografia de clau pública en lloc de contrasenyes. Quan un usuari es registra, el dispositiu genera un parell de claus: la clau pública s’envia al servidor, i la clau privada mai surt del dispositiu.
- FIDO2 és l’estàndard complet (definit per la FIDO Alliance) que inclou WebAuthn (la part del navegador) i CTAP (el protocol de comunicació amb autenticadors externs com claus USB).
- Passkeys són la implementació pràctica d’usuari de FIDO2: credencials WebAuthn que es sincronitzen entre dispositius a través del clauer del sistema operatiu. Això resol el problema històric de l’autenticació sense contrasenya: què passa si perds el dispositiu?
Durant l’autenticació, el servidor envia un repte (challenge) aleatori. El dispositiu signa aquest repte amb la clau privada, i el servidor verifica la signatura amb la clau pública emmagatzemada. Això elimina completament els atacs de phishing tradicionals: no hi ha contrasenya per robar, i les credencials estan vinculades al domini específic.
La implementació requereix suport tant al client (navegador) com al servidor. El client utilitza l’API WebAuthn del navegador per crear credencials i generar assertions. El servidor ha de validar aquestes assertions, verificant signatures i comprovant que el repte, origen i altres paràmetres són correctes.
L’adopció és àmplia i creixent: molts serveis web populars ofereixen login amb passkeys, i els principals sistemes operatius l’integren de forma nativa. Els navegadors moderns suporten conditional UI (o passkey autofill), que mostra passkeys disponibles directament en el camp d’usuari, proporcionant una experiència similar a l’autocompletat de contrasenyes. L’autenticació cross-device permet utilitzar el telèfon per autenticar-se en un ordinador escanejant un codi QR, útil quan els passkeys no estan sincronitzats entre dispositius.
La validació del servidor és la part més delicada i no s’ha d’implementar des de zero. Cal verificar la signatura, el challenge, l’origen i el RPID; qualsevol omissió crea vulnerabilitats difícils de detectar. Utilitza una biblioteca testada per al teu llenguatge (SimpleWebAuthn, webauthn4j, etc.) per a tota la lògica del costat del servidor.
Seguretat Web
Cross-Origin Resource Sharing (CORS)
La Same-Origin Policy és una restricció fonamental dels navegadors: el JavaScript d’una pàgina només pot fer peticions a la mateixa origen (protocol, domini i port). Això protegeix contra molts atacs on un site maliciós intenta llegir dades d’un altre site on l’usuari ha iniciat sessió.
CORS és una peça que tot desenvolupador web trobarà aviat o tard: qualsevol SPA que consumeixi una API en un domini diferent (per exemple, app.example.com cridant api.example.com) necessita CORS configurat correctament. CORS relaxa aquesta restricció de manera controlada, permetent als servidors especificar quins origens poden accedir als seus recursos. Quan el JavaScript fa una petició cross-origin, el navegador primer pot enviar una petició OPTIONS (preflight) preguntant al servidor si permet aquesta petició. El servidor respon amb headers indicant què està permès.
L’header Access-Control-Allow-Origin especifica quins origens poden accedir al recurs. Pot ser un origen específic o el wildcard *. Tanmateix, si la petició inclou credencials (cookies), no es pot utilitzar *: s’ha d’especificar l’origen exacte. Access-Control-Allow-Methods i Access-Control-Allow-Headers controlen quins mètodes HTTP i headers personalitzats estan permesos.
Access-Control-Allow-Credentials: true permet que les peticions incloguin cookies i headers d’autorització. Això és perillós si es combina amb origens massa permissius. Un error comú és reflectir automàticament l’origen de la petició al header Access-Control-Allow-Origin quan s’accepten credencials, permetent efectivament qualsevol origin autenticar-se.
Les peticions simples (GET, HEAD, POST amb content-types específics i sense headers personalitzats) no requereixen preflight. Això pot ser sorprenent per a desenvolupadors que esperen que totes les peticions cross-origin es verifiquin primer. És important dissenyar APIs assumint que aquestes peticions simples poden arribar de qualsevol origen.
Cross-Site Scripting (XSS)
XSS és una de les vulnerabilitats web més freqüents, present de manera recurrent al OWASP Top 10. Consisteix en l’injecció de JavaScript maliciós en pàgines web vistes per altres usuaris. Hi ha tres tipus principals. El Reflected XSS injecta script en paràmetres de URL o formularis que es reflecteixen immediatament a la pàgina. El Stored XSS emmagatzema el script a la base de dades (per exemple, en comentaris) i l’executa cada vegada que es visualitza. El DOM-based XSS manipula el DOM del client sense involucrar el servidor.
La Content Security Policy (CSP) és la defensa més potent. És un header que especifica quines fonts de contingut són de confiança. script-src 'self' només permet scripts del mateix origen. script-src https://trusted-cdn.com permet scripts d’un CDN específic. Però el millor enfocament modern són els nonces: el servidor genera un valor aleatori per cada resposta i només executa scripts amb aquest nonce.
Content-Security-Policy: script-src 'nonce-r4nd0m123'
<script nonce="r4nd0m123">...</script>
Això invalida completament XSS injectat perquè l’atacant no pot conèixer el nonce. La directiva strict-dynamic permet que scripts amb nonces creïn nous scripts dinàmicament, facilitant l’ús de frameworks moderns.
Els frameworks frontend moderns (React, Vue, Angular, Svelte) proporcionen escapament automàtic per defecte. Les expressions en templates s’escapen automàticament convertint caràcters perillosos com < en entitats HTML. Tanmateix, funcions que permeten inserir HTML “raw” desactiven aquesta protecció i només s’han d’utilitzar amb contingut de confiança.
La Trusted Types API força que l’assignació a sinks perillosos (innerHTML, eval, etc.) només accepti objectes Trusted Type. Això converteix XSS d’un problema de revisar tot el codi a un problema de revisar els punts específics on es creen aquests objectes de confiança.
La sanitització és necessària quan has de permetre HTML limitat (per exemple, formatació en comentaris). Les llibreries de sanitització eliminen elements i atributs perillosos mentre preserven el formatatge segur. Mai intentis implementar el teu propi sanititzador: és extremadament difícil fer-ho correctament.
Cross-Site Request Forgery (CSRF)
CSRF explota que els navegadors adjunten automàticament cookies a les peticions. Un site maliciós pot fer que el teu navegador enviï una petició al teu banc (amb les teves cookies de sessió) transferint diners a l’atacant. L’usuari està autenticat, la petició és vàlida tècnicament, però no intencionada.
SameSite cookies són la defensa més simple i efectiva. SameSite=Strict impedeix completament que la cookie s’enviï en peticions cross-site. SameSite=Lax permet-la en navegacions top-level (seguir enllaços) però no en formularis POST o fetch requests cross-site. Per a la majoria d’aplicacions, Lax proporciona un bon equilibri entre seguretat i usabilitat. De fet, des de 2020, Chrome, Firefox i Edge apliquen SameSite=Lax per defecte a les cookies que no especifiquen el valor, cosa que ha reduït dràsticament la superfície d’atac CSRF a la web.
Els tokens CSRF són un enfocament més tradicional. El servidor inclou un token aleatori en formularis o el proporciona a l’aplicació. Quan el formulari es submits, el token s’inclou (típicament com a camp ocult o header). El servidor verifica que el token coincideix amb el de la sessió. Com que l’atacant no pot llegir el token (per Same-Origin Policy), no pot crear peticions vàlides.
El pattern Double Submit Cookie emmagatzema el token tant en una cookie com en un lloc accessible a JavaScript (localStorage o el DOM). Quan es fa una petició, JavaScript llegeix el token i l’envia com a header o paràmetre. El servidor verifica que coincideixin. Això funciona perquè un atacant no pot establir cookies per al teu domini ni llegir-les.
Per a APIs JSON, un header personalitzat com X-Requested-With: XMLHttpRequest pot ser suficient. Els navegadors no permeten que sites arbitraris afegeixin headers personalitzats en peticions cross-origin (sense CORS), així que la presència d’aquest header demostra que la petició es va originar des del JavaScript del teu site.
Injection Attacks
La SQL injection insereix SQL maliciós en queries, potencialment llegint, modificant o eliminant dades. El input ' OR '1'='1 en un camp d’usuari podria convertir SELECT * FROM users WHERE username='$input' en SELECT * FROM users WHERE username='' OR '1'='1', retornant tots els usuaris.
Les prepared statements (també anomenades parameterized queries) són la solució definitiva. En lloc de concatenar strings per construir SQL, utilitzes placeholders que el driver de base de dades tracta com a dades, mai com a codi:
cursor.execute("SELECT * FROM users WHERE username = ?", (user_input,))
El driver automàticament escapa el input apropiadament per al dialecte SQL específic. És impossible injectar SQL perquè el input mai s’interpreta com a codi.
Els ORMs (Object-Relational Mappers) construeixen queries utilitzant mètodes que internament usen prepared statements. Tanmateix, ofereixen “escape hatches” per a raw SQL que poden ser vulnerables si no s’utilitzen amb cura. Les funcions de l’ORM són segures, les raw queries requereixen la mateixa atenció que el SQL directe.
El principi de mínim privilegi s’aplica a les credencials de base de dades. L’aplicació no hauria de connectar-se amb un usuari que té permisos DROP TABLE o CREATE USER. Si l’aplicació només llegeix i escriu dades normals, les seves credencials haurien de reflectir només aquests permisos.
Command injection executa comandes del sistema operatiu. Si l’aplicació utilitza input d’usuari per construir comandes (per exemple, executant ping $userInput), un atacant pot injectar ;rm -rf /. La millor defensa és evitar completament l’execució de comandes del shell amb input d’usuari. Si és absolutament necessari, utilitza llibreries que accepten arguments com a llista en lloc de string, i valida estrictament l’input contra una whitelist.
LDAP, XML i NoSQL injection són anàlegs a SQL injection en altres llenguatges de query. Els principis són els mateixos: mai construir queries concatenant strings, utilitzar sempre APIs que separen codi de dades, i validar l’input.
Security Headers
El navegador és molt permissiu per defecte: executa qualsevol script que trobi a la pàgina, carrega recursos de qualsevol origen, permet que la teva pàgina es mostri dins d’un iframe en qualsevol altre site, i intenta endevinar el tipus de contingut si no està clar. Els headers de seguretat són instruccions explícites amb les quals el servidor li diu al navegador quins comportaments restringir en les teves pàgines.
HTTP Strict Transport Security (HSTS) obliga els navegadors a utilitzar sempre HTTPS amb el teu site:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Després de rebre aquest header, el navegador converteix automàticament totes les peticions HTTP en HTTPS durant un any. includeSubDomains aplica això també als subdominis. preload permet incloure el teu domini en llistes hardcoded dels navegadors, protegint fins i tot la primera visita.
Content Security Policy ja s’ha discutit en XSS, però també controla altres recursos: img-src, style-src, connect-src, etc. upgrade-insecure-requests instrueix el navegador a carregar tots els recursos per HTTPS fins i tot si l’HTML especifica HTTP.
X-Frame-Options: DENY impedeix que la teva pàgina es carregui en un iframe, protegint contra clickjacking. SAMEORIGIN permet iframes del mateix origen. La directiva CSP frame-ancestors és més flexible i hauria de preferir-se si ja utilitzes CSP.
X-Content-Type-Options: nosniff impedeix que els navegadors “endevinin” el tipus MIME del contingut. Sense això, un navegador podria interpretar un arxiu JSON com a HTML si sembla contingut HTML, potencialment executant scripts.
Referrer-Policy controla quanta informació de la URL anterior s’inclou en la petició següent. strict-origin-when-cross-origin envia la URL completa per a peticions same-origin però només l’origen per a cross-origin, i res si baixa de HTTPS a HTTP.
Permissions-Policy (anteriorment Feature-Policy) controla l’accés a funcionalitats del navegador com la càmera, micròfon, geolocalització, etc. Permissions-Policy: camera=(), microphone=() desactiva completament aquestes funcionalitats.
Configurar els headers no és suficient: cal verificar que arriben al navegador en producció. Les configuracions de proxy, CDN o balancejador de càrrega poden eliminar-los silenciosament. Eines com securityheaders.com permeten comprovar-ho en un minut. Per a CSP en particular, és recomanable activar-lo primer en mode Content-Security-Policy-Report-Only per detectar violacions sense bloquejar res, i passar a mode d’aplicació un cop validat.
Clickjacking i UI Redressing
El clickjacking superimposa contingut invisible sobre la teva pàgina, enganyant els usuaris perquè facin clic en coses que no veuen. Per exemple, un iframe invisible amb el teu site col·locat sobre un joc. L’usuari creu que està jugant però està fent clic en botons del teu site.
X-Frame-Options i la directiva CSP frame-ancestors són les defenses primàries. frame-ancestors 'none' és equivalent a X-Frame-Options: DENY. frame-ancestors 'self' https://trusted.com permet específicament iframes del teu site i un site de confiança.
Les tècniques JavaScript de “frame busting” (detectar si estàs en un iframe i sortir-ne) són menys fiables. Els atacants han trobat maneres de desactivar-les utilitzant l’atribut sandbox dels iframes o altres tècniques. Els headers del servidor són més robusts perquè el navegador els respecta abans de carregar qualsevol JavaScript.
Web Application Firewalls (WAF)
Un Web Application Firewall (WAF) és una capa de protecció que filtra, monitoritza i bloqueja tràfic HTTP maliciós entre internet i l’aplicació web. Opera a nivell 7 (capa d’aplicació) del model OSI, permetent inspeccionar el contingut de les peticions. La majoria de sites web amb tràfic significatiu utilitzen algun tipus de WAF avui dia.
Els WAFs poden desplegar-se a l’edge (CDN/cloud) o a nivell d’aplicació. Els proveïdors de CDN ofereixen WAFs gestionats que filtren tràfic abans que arribi a l’origen. En el món open source, ModSecurity (integrable amb Nginx o Apache) és la referència històrica.
L’OWASP Core Rule Set (CRS) és un conjunt de regles genèriques que protegeix contra els atacs més comuns: SQL injection, XSS, LFI/RFI, i altres. Proporciona un punt de partida sòlid, però sovint requereix tuning per evitar falsos positius en aplicacions específiques.
Els WAFs moderns van més enllà de regles estàtiques, incorporant machine learning per detectar anomalies i rate limiting integrat. Tanmateix, no són una solució màgica: són una capa de defensa adicional, no un substitut de codi segur. Atacants sofisticats poden trobar maneres d’evadir regles WAF, especialment si el WAF no està correctament configurat.
Seguretat en la Càrrega de Fitxers
La càrrega de fitxers és una de les funcionalitats web amb més superfície d’atac. Un fitxer pujat pot ser un webshell (script executable al servidor), explotar vulnerabilitats en parsers d’imatge o PDF, o contenir contingut que travessi el sistema de fitxers del servidor.
El path traversal via nom de fitxer és un dels atacs més immediats: si un atacant puja un fitxer amb el nom ../../config/database.yml i el servidor el guarda utilitzant el nom original, pot sobreescriure fitxers arbitraris. La defensa és simple però crítica: mai s’ha d’utilitzar el nom de fitxer original per a l’emmagatzematge. Cal extreure únicament el nom base (sense components de directori) i, millor encara, substituir-lo completament per un nom generat al servidor (UUID).
La validació del tipus de fitxer no pot basar-se en la capçalera Content-Type enviada pel client, que és completament controlada per l’atacant. Cal inspeccionar el contingut real del fitxer (els primers bytes o magic bytes), ja que cada format té una signatura característica. Existeixen llibreries per a aquesta tasca en tots els llenguatges principals; no s’ha d’implementar manualment.
L’emmagatzematge fora del web root és fonamental. Si els fitxers pujats es guarden en un directori servit directament pel servidor web, un atacant pot pujar un script i executar-lo simplement accedint a la seva URL. Els fitxers han d’emmagatzemar-se en un directori no accessible directament (fora del document root) o en un servei d’objectes extern (S3, Azure Blob). Quan cal servir-los, s’ha de fer a través d’un endpoint controlat que estableixi la capçalera Content-Disposition: attachment per forçar la descàrrega en lloc de l’execució.
El límit de mida s’ha d’imposar al servidor abans de llegir el payload complet, no al client. Un atacant pot enviar una petició sense les restriccions del navegador.
Si l’aplicació accepta arxius comprimits (ZIP, tar), cal limitar la mida descomprimida i el nombre d’entrades abans d’extreure res: un arxiu zip bomb pot tenir 42 KB comprimits que s’expandeixen a centenars de GB. En entorns de producció crítics, els fitxers pujats haurien de passar per un escàner antivirus (ClamAV o un servei cloud) abans de quedar disponibles per a altres usuaris.
Subresource Integrity
Els CDNs permeten carregar llibreries populars ràpidament des de servidors optimitzats. Tanmateix, això introdueix un punt de confiança extern. Si el CDN és compromès o alterat, scripts maliciosos es carreguen en el teu site.
Subresource Integrity (SRI) proporciona protecció calculant un hash criptogràfic del script o stylesheet esperat i incloent-lo en el tag:
<script src="https://cdn.example.com/library.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous"></script>
El navegador descarrega l’arxiu, calcula el seu hash, i només l’executa si coincideix. Si el CDN serveix un arxiu diferent (maliciós o simplement una versió diferent), el navegador el bloqueja i llença un error. L’atribut crossorigin="anonymous" és necessari per permetre que el navegador llegeixi el contingut per validar-lo.
SRI és especialment important per a scripts crítics com frameworks. Un React compromès podria capturar totes les interaccions de l’usuari. CDNs populars com cdnjs i jsDelivr proporcionen hashes SRI automàticament al costat de cada URL de recurs.
La limitació d’SRI és que requereix saber el hash exacte per endavant. Si el CDN actualitza l’arxiu (fins i tot per fixes de seguretat), el hash canvia i la teva pàgina es trenca. Això requereix processos per actualitzar hashes quan actualitzes dependències.
Third-Party Scripts
Els scripts de tercers (analytics, ads, chat widgets) s’executen amb els mateixos privilegis que el teu propi codi. Poden accedir a localStorage, fer peticions, modificar el DOM, i capturar inputs de l’usuari. Cada script de tercer és un risc potencial.
Content Security Policy ajuda limitant d’on es poden carregar scripts. Si només permets scripts del teu domini i CDNs específics de confiança, scripts arbitraris no poden injectar-se. Tanmateix, scripts permesos encara tenen accés complet.
Els sandbox iframes proporcionen aïllament més fort. Carregant un script de tercer en un iframe amb l’atribut sandbox, pots limitar què pot fer: sandbox="allow-scripts" permet scripts però impedeix navegació, formularis, modals, etc. Els iframes sandbox no poden accedir a localStorage del parent ni llegir cookies.
Tanmateix, molts scripts de tercers requereixen accés al DOM principal per funcionar (per exemple, un widget de chat), fent sandbox impracticable. En aquests casos, l’auditoria regular és essencial. Què fa realment aquest script? Envia dades a servidors externs? Compleix amb GDPR?
Les solucions de server-side tagging mouen l’execució de scripts de tercers al servidor. El client envia dades event al teu servidor, que després les forwarda als serveis de tercers. Això proporciona control sobre quines dades es comparteixen i elimina scripts de tercers del navegador.
Seguretat d’APIs
Autenticació d’APIs
L’autenticació basada en formularis i redireccionaments cap a una pàgina de login funciona per a humans en un navegador, però no per a programes que es criden entre ells. Una API consumida per un servei backend, una app mòbil o un script no té usuari interactiu i no pot seguir un flux de login. Cal un mecanisme diferent.
Les APIs RESTful utilitzen Bearer tokens, típicament JWTs, inclosos a l’header Authorization: Bearer <token> de cada petició. El servidor valida el token criptogràficament sense consultar cap base de dades de sessions, fent el sistema stateless.
Les API keys identifiquen el client o projecte que fa peticions. A diferència dels tokens d’usuari, no representen un usuari específic sinó una aplicació. S’han de transmetre sempre en headers (com X-API-Key), mai en la URL, perquè les URLs es poden guardar en logs, historial de navegador, etc. Les API keys haurien de rotar-se periòdicament, i els sistemes haurien de permetre múltiples keys actives simultànies per facilitar la rotació sense downtime.
Mutual TLS (mTLS) proporciona autenticació bidireccional amb certificats. No només el client verifica la identitat del servidor (com en TLS normal), sinó que el servidor també verifica la identitat del client mitjançant el seu certificat. Això és comú en comunicació microserveis dins d’un clúster on cada servei té el seu propi certificat, sovint gestionat per una service mesh com Istio o Linkerd.
OAuth Client Credentials Flow és ideal per a autenticació màquina-a-màquina. El client (servei) s’autentica directament amb l’authorization server utilitzant les seves credencials i obté un token d’accés. Aquest flow no involucra cap usuari; el token representa el servei mateix.
Mai s’han d’incloure credencials a la URL. Les URLs apareixen en logs de servidor, historials de navegador, capçaleres Referer i eines de monitoratge, sovint en text pla. Tant API keys com tokens han de viatjar sempre en headers HTTP, no com a query parameters.
Rate Limiting i Throttling
El rate limiting protegeix les APIs contra abús, atacs DoS i usage excessiu que pot afectar altres usuaris. Els límits es poden aplicar per IP, per usuari autenticat, per API key, o combinacions d’aquests. Els límits típics són peticions per segon, per minut o per hora.
L’algoritme token bucket és popular: cada client té un “cubell” amb una capacitat màxima de tokens. Els tokens es regeneren a una taxa constant. Cada petició consumeix un token. Si el cubell està buit, la petició es rebutja o es posa en cua. Això permet bursts curts mentre manté una taxa promig.
L’algoritme sliding window manté un comptador per cada finestra de temps (per exemple, cada minut). Quan una petició arriba, es comprova el comptador de la finestra actual i possiblement la anterior. Això proporciona un límit més suau que fixed windows, que poden permetre el doble de peticions al voltant de límits de finestra.
Els headers de resposta comuniquen l’estat dels límits al client:
X-RateLimit-Limit: Màxim permèsX-RateLimit-Remaining: Quantes quedenX-RateLimit-Reset: Quan es restableix el límitRetry-After: Quan tornar a intentar-ho després d’un 429
La implementació pot ser en middleware de l’aplicació, en un API gateway, o utilitzant un store compartit en memòria per a arquitectures distribuïdes. Redis és l’elecció més habitual perquè les seves operacions atòmiques i expiració automàtica de claus s’adapten perfectament a rate limiting.
L’error de disseny més freqüent és aplicar rate limiting per IP en lloc de per usuari o per API key. Un atacant distribuït pot enviar peticions des de milers d’IPs sense activar el límit; alhora, un usuari legítim darrere d’un NAT corporatiu pot ser bloquejat per compartir IP amb desenes de companys.
Input Validation
La validació d’input és la primera línia de defensa contra molts atacs. El principi de whitelist és fonamental: defineix què és acceptable en lloc d’intentar detectar què és maliciós. Per exemple, si esperes un codi postal espanyol, valida que sigui exactament 5 dígits, no intentes detectar patterns d’atac.
JSON Schema i OpenAPI permeten definir l’estructura esperada de les peticions declarativament. Llibreries de validació com Zod (TypeScript), Joi (Node.js) o ajv (JSON Schema genèric) poden automàticament rebutjar peticions que no compleixin l’schema. Això centralitza la validació i la fa consistent entre endpoints.
La verificació de tipus és especialment important en llenguatges dinàmics. Si esperes un número, converteix-lo explícitament i gestiona errors de conversió. No assumeixis que perquè un camp es diu age contindrà un número.
Establir límits és crucial: longitud màxima de strings (prevé buffer overflows i DoS), ranges numèrics, tamany màxim de payload (prevé DoS de memòria). Els valors fora d’aquests límits haurien de ser rebutjats immediatament amb errors 400 clars.
La validació hauria d’ocórrer el més aviat possible en el pipeline de petició, idealment en middleware abans d’arribar a la lògica de negoci. Això manté el codi de negoci net i assegura que la validació no s’oblidi accidentalment.
Un cas especial de validació d’input és la prevenció de Server-Side Request Forgery (SSRF): quan l’aplicació fa peticions HTTP a URLs proporcionades per l’usuari (per exemple, per obtenir una imatge o un webhook), un atacant pot apuntar a serveis interns (http://localhost, http://169.254.169.254 per a metadades cloud, rangs privats com 10.0.0.0/8 o 192.168.0.0/16). Les defenses principals són: mantenir una whitelist de dominis o IPs permesos, rebutjar adreces que resolguin a rangs privats o localhost, no seguir redireccions cegament (una URL pública pot redirigir a una interna), i limitar els protocols acceptats (només HTTP/HTTPS, mai file:// o gopher://).
IDOR i Falles de Lògica de Negoci
La validació d’entrades comprova la forma de les dades. Però un cop les dades són vàlides, cal comprovar si l’usuari té dret a accedir a l’objecte concret que identifiquen.
L’IDOR (Insecure Direct Object Reference) és una vulnerabilitat d’autorització: el servidor utilitza un identificador controlat per l’usuari (un paràmetre de URL, un camp del cos de la petició) per accedir a un recurs, sense verificar que l’usuari autenticat té dret sobre aquell recurs específic. L’exemple canònic és:
GET /api/comandes/1234 → retorna la comanda de l'usuari actual ✓
GET /api/comandes/1235 → retorna la comanda d'un altre usuari ✗
Un atacant simplement incrementa l’identificador i accedeix a dades d’altres usuaris. Això és escalació horitzontal de privilegis: un usuari amb els mateixos permisos que un altre accedeix a les seves dades.
L’error de fons és confondre autenticació amb autorització a nivell d’objecte: saber qui ets no implica saber si pots tocar aquest recurs concret. La correcció és sempre filtrar la consulta per l’identitat de l’usuari autenticat:
-- Insegur: qualsevol usuari autenticat pot accedir a qualsevol comanda
SELECT * FROM comandes WHERE id = ?
-- Segur: la consulta retorna el recurs només si pertany a l'usuari actual
SELECT * FROM comandes WHERE id = ? AND usuari_id = ?
Mai s’ha de fer un fetch i després comprovar: si la consulta retorna dades d’un altre usuari, la informació ja ha estat llegida. El filtre ha de ser part de la consulta.
L’ús d’identificadors no seqüencials (UUIDs) dificulta l’enumeració, però no és una solució: un atacant pot obtenir IDs legítims d’altres maneres. La comprovació d’autorització és sempre necessària.
La mass assignment és una variant relacionada: si el servidor accepta tots els camps del cos de la petició i els assigna directament al model de dades, un atacant pot enviar camps que l’API mai hauria d’acceptar d’un client, com is_admin: true o preu: 0. La solució és whitelistar explícitament quins camps pot modificar un usuari en cada context.
Més àmpliament, les falles de lògica de negoci són situacions en què l’aplicació no fa complir les seves pròpies regles en tots els camins d’execució. Exemples: aplicar un codi de descompte dues vegades, saltar-se un pas obligatori d’un flux, o accedir a un recurs en estat “esborrany” via una crida directa a l’API que la interfície d’usuari mai exposa. Aquestes falles no les detecten les eines automàtiques i requereixen revisió manual i proves amb múltiples comptes d’usuari que intentin accedir als recursos dels altres.
API Gateways
Un API gateway és un reverse proxy que centralitza funcionalitat transversal: autenticació de tokens, rate limiting, validació de peticions contra un esquema OpenAPI, i logging de tot el tràfic. Des de la perspectiva de seguretat, permet aplicar aquests controls una sola vegada en lloc de duplicar-los a cada microservei. Exemples àmpliament adoptats inclouen Kong, Envoy (que també és la base de service meshes com Istio) i Traefik, tots open source. Pràcticament qualsevol arquitectura de microserveis en producció utilitza algun tipus de gateway.
Seguretat en el Desenvolupament Modern
Secrets Management
El problema dels secrets (contrasenyes, API keys, claus de xifratge) és que el codi s’ha de poder executar però no ha de contenir els secrets directament. Fer commit de secrets al repositori és perillós: el git history manté els secrets fins i tot si es eliminen després, i els repositoris privats poden esdevenir públics accidentalment o ser compromesos.
Per a desenvolupament local, les variables d’entorn són un primer pas raonable. Un arxiu .env (que està al .gitignore) conté els secrets, i l’aplicació els llegeix a l’inici. Això separa secrets del codi, però encara requereix distribuir els secrets als desenvolupadors de manera segura.
Els vaults (gestors de secrets centralitzats) van més enllà. HashiCorp Vault (open source) és la solució de referència, i els cloud providers ofereixen les seves pròpies alternatives gestionades. En lloc d’emmagatzemar secrets en arxius o variables d’entorn, l’aplicació consulta el vault en runtime. Això proporciona diversos avantatges: auditoria completa de qui accedeix quins secrets, rotació automàtica de secrets sense redesplegar aplicacions, i control d’accés granular.
En entorns Kubernetes, els Secrets nadius xifren dades at-rest utilitzant la clau de xifratge del clúster. Tanmateix, per defecte aquesta clau està emmagatzemada en etcd sense xifratge addicional. La configuració d’encryption at rest és essencial per a clústers de producció. Solucions d’External Secrets permeten fer commit de secrets xifrats al repositori, que només poden ser desxifrats pel clúster.
Per xifrar arxius de configuració directament en git, eines com SOPS (de Mozilla) utilitzen claus de proveïdors cloud o xifratge modern amb age (substitut lleuger de GPG). Això permet versionar configuració sensible de manera segura.
Les bones pràctiques inclouen mai fer commit de secrets (utilitzar hooks de git per detectar-los), rotar secrets regularment i automàticament quan és possible, i implementar accés basat en rols on només els serveis que necessiten un secret específic poden accedir-hi. Els secrets haurien de tenir scopes mínims: una API key per a un servei específic, no una master key que fa tot.
El risc no desapareix eliminant el secret del repositori. Git manté l’historial complet: un secret que va existir en un commit passat segueix accessible per a qualsevol que cloni el repositori. Si un secret s’ha exposat, la resposta correcta és revocar-lo i generar-ne un de nou immediatament, no simplement eliminar-lo del codi.
Les 12 factors i la seguretat
La metodologia dels 12 factors és la referència dominant per al disseny d’aplicacions cloud-native. Diversos factors tenen implicacions directes de seguretat que els desenvolupadors sovint passen per alt en aplicar-la.
Factor III — Configuració: la configuració específica de l’entorn (credencials, URLs de serveis externs, secrets) ha d’emmagatzemar-se en variables d’entorn, mai al codi font. Un arxiu application.properties o config.yml amb credencials de producció al repositori és una de les filtracions més freqüents. La secció anterior de gestió de secrets implementa aquest principi en detall.
Factor VI — Processos sense estat: l’aplicació ha de ser stateless i no compartir res entre peticions. Les sessions no poden viure a la memòria d’una instància concreta: han d’emmagatzemar-se en serveis de suport (Redis, base de dades). A més de facilitar l’escalat horitzontal, això té un benefici de seguretat directe: si una instància és compromesa i reiniciada, no arrossega sessions actives en memòria.
Factor X — Paritat dev/prod: l’entorn de desenvolupament ha de ser el més similar possible a producció. Moltes vulnerabilitats es descobreixen únicament en producció perquè els entorns de desenvolupament i staging desactiven controls de seguretat per comoditat: bases de dades sense TLS, HTTPS desactivat, middlewares d’autenticació comentats, o configuració CORS permissiva (*). Cada diferència entre entorns és una oportunitat perquè un problema passi desapercebut fins a producció.
Factor XI — Logs com a fluxos d’esdeveniments: l’aplicació ha d’escriure logs a stdout i deixar que la infraestructura els reculli i encamini. Això facilita l’agregació centralitzada de logs descrita a la secció de Logging i Monitoring: si cada servei decideix on escriure els seus logs, coordinar-los per a anàlisi de seguretat és molt més difícil.
Factor XII — Processos d’administració: les tasques puntuals (migracions de base de dades, scripts d’inicialització) han d’executar-se en el mateix entorn i amb el mateix codi que l’aplicació. Executar scripts d’administració localment amb credencials elevades que bypassen el model d’autorització de l’aplicació és un vector d’escalació de privilegis freqüent: si el script fa el que l’aplicació no permetria, s’està saltant un control de seguretat deliberat.
Dependency Security
El programari modern depèn de centenars o milers de paquets de tercers. Cada dependència és codi que no has escrit però que s’executa amb els mateixos privilegis que el teu. Els supply chain attacks exploten això comprometent dependències populars. L’atac a event-stream (2018) o ua-parser-js (2021) van injectar codi maliciós en paquets amb milions de descàrregues.
Les eines d’audit de dependències (npm audit, pip audit, cargo audit) escanegen les dependències contra bases de dades de vulnerabilitats conegudes. Haurien d’executar-se regularment en CI/CD i bloquejar deployments si es troben vulnerabilitats crítiques. Tanmateix, aquestes eines només detecten vulnerabilitats publicades, no codi maliciós encara no descobert.
Eines d’actualització automàtica de dependències com Renovate van més enllà, creant automàticament pull requests per actualitzar dependències quan es publiquen fixes de seguretat. Això redueix el window de vulnerabilitat però requereix processos de test robusts per assegurar que les actualitzacions no trenquin funcionalitat.
Els lock files (package-lock.json, yarn.lock, pnpm-lock.json) són essencials per a seguretat reproducible. Sense ells, npm install podria instal·lar versions diferents cada vegada, potencialment incloent una versió compromesa publicada després del teu últim test. Els lock files asseguren que tothom (desenvolupadors, CI, producció) utilitza exactament les mateixes versions.
La verificació d’integritat va un pas més enllà. npm utilitza checksums SHA-512 per verificar que el paquet descarregat no ha estat alterat. Algunes organitzacions van més lluny, verificant signatures criptogràfiques de paquets quan estan disponibles.
El Software Bill of Materials (SBOM) és una llista completa de tots els components del teu programari, incloent dependències transitives. Formats com SPDX o CycloneDX permeten generar SBOMs automàticament. Això és cada vegada més un requisit de compliance i facilita respondre ràpidament quan es descobreix una vulnerabilitat: pots saber immediatament si estàs afectat.
El framework SLSA (Supply chain Levels for Software Artifacts) defineix nivells de maduresa per a la seguretat de la cadena de subministrament. Des del nivell 1 (documentació bàsica del build) fins al nivell 4 (builds hermètics amb verificació completa), proporciona un camí clar per millorar. L’ecosistema Sigstore implementa SLSA amb components per signar artefactes, emetre certificats efímers, i mantenir registres de transparència. Això permet verificar la procedència d’artefactes sense gestionar claus de llarga durada.
Actualitzar quan es publica una vulnerabilitat no és suficient si no s’han revisat les dependències transitives. La majoria de vulnerabilitats crítiques en producció provenen de paquets que no has escollit directament, sinó d’una dependència d’una dependència. Les eines d’audit modernes mostren l’arbre complet i indiquen exactament quin paquet directe arrossega el paquet vulnerable.
Container Security
Els containers empaqueten aplicacions amb les seves dependències, però això significa que també empaqueten vulnerabilitats. La imatge base és crítica: una imatge amb un sistema operatiu complet pot contenir centenars de paquets que no necessites, cada un una possible font de vulnerabilitats. Imatges mínimes com Alpine Linux o Distroless contenen només el mínim necessari per executar l’aplicació. Per a llenguatges compilats com Go o Rust, fins i tot es poden utilitzar imatges scratch (completament buides) amb només el binari estàtic.
L’escaneig de vulnerabilitats d’imatges hauria de ser part del pipeline CI/CD. Eines com Trivy analitzen imatges per vulnerabilitats conegudes en paquets del sistema i dependències de llenguatge. Poden configurar-se per bloquejar el build si es troben vulnerabilitats crítiques.
Els multi-stage builds milloren la seguretat minimitzant el que va a la imatge final. La primera stage compila el codi amb totes les build tools necessàries. L’stage final només copia el binari compilat i les dependencies runtime a una imatge mínima. Això elimina compiladors, build tools i codi font de la imatge de producció.
Executar containers com a root és perillós. Si un atacant aconsegueix escapar del container, tindrà privilegis de root a l’host. Els Dockerfiles haurien d’incloure USER nonroot o crear un usuari específic. Kubernetes permet enforçar això amb PodSecurityStandards que rebutgen pods que intenten executar com a root.
Els secrets mai haurien de formar part de la imatge (vegeu Secrets Management). Les capes de Docker no s’eliminen mai, fins i tot si s’esborren en passos posteriors: si un RUN afegeix un secret (una clau SSH per clonar un repositori privat, per exemple) i una capa posterior l’elimina, el secret continua present en la capa original i és recuperable per qualsevol amb accés a la imatge. Els secrets han d’arribar en temps d’execució via variables d’entorn o mounts, mai com a pas del build.
Les imatges haurien d’actualitzar-se regularment, no només quan es canvia l’aplicació. Una imatge construïda fa sis mesos pot contenir vulnerabilitats que s’han descobert des de llavors. Rebuilds periòdics utilitzant la imatge base més recent asseguren que els patches de seguretat s’apliquen.
TLS/HTTPS
TLS (Transport Layer Security) xifra les comunicacions entre client i servidor, protegint contra espionatge i man-in-the-middle attacks. TLS 1.3 és la versió actual, llançada el 2018. Elimina cipher suites i opcions insegures, redueix la latència del handshake, i xifra més parts de la negociació. TLS 1.2 és el mínim acceptable en 2025; TLS 1.0 i 1.1 estan deprecated i han estat eliminats dels navegadors principals des de 2020.
Let’s Encrypt va revolucionar l’adopció d’HTTPS proporcionant certificats gratuïts i automatitzats. El protocol ACME permet als servidors demanar i renovar certificats automàticament sense intervenció manual, gestionant tot el cicle de vida dels certificats.
La configuració del servidor TLS és crucial. Cipher suites modernes com TLS_AES_128_GCM_SHA256 proporcionen xifratge fort eficient. Suites antigues amb RC4 o DES són vulnerables i haurien de desactivar-se. Perfect Forward Secrecy (PFS) utilitza claus efímeres per a cada sessió, assegurant que fins i tot si la clau privada del servidor es compromet en el futur, sessions passades no es poden desxifrar.
OCSP Stapling millora performance i privacitat. Sense ell, el client ha de contactar l’autoritat de certificació per verificar que el certificat no ha estat revocat, afegint latència i permetent a la CA trackejar quins sites visita l’usuari. Amb stapling, el servidor consulta l’estat de revocació i l’inclou en el handshake TLS, eliminant aquesta round-trip.
Certificate pinning en aplicacions mòbils significa hardcodejar la clau pública esperada del servidor. Això protegeix contra CAs compromeses que podrien emetre certificats falsos per al teu domini. Tanmateix, és perillós: si necessites rotar certificats i no has planejat adequadament (incloent backup pins), pots deixar usuaris sense poder connectar. Per això, moltes organitzacions pinen claus intermèdies en lloc de leaf certificates, donant més flexibilitat.
CI/CD Security
El pipeline CI/CD té accés a codi font, secrets, i permisos de deploy. És un objectiu d’alt valor per atacants. El SolarWinds hack (2020) va comprometre el build system per injectar malware en actualitzacions de programari, afectant milers d’organitzacions.
SAST (Static Application Security Testing) analitza codi font buscant patrons vulnerables, detectant SQL injection, XSS, i altres vulnerabilitats comunes. Eines open source com Semgrep o CodeQL són àmpliament utilitzades. Aquestes eines haurien d’executar-se en cada pull request, bloquejant merge si es troben vulnerabilitats crítiques.
DAST (Dynamic Application Security Testing) testeja l’aplicació en execució. Eines com OWASP ZAP (open source) escanegen l’aplicació desplegada i envien inputs maliciosos per detectar vulnerabilitats. DAST detecta problemes que SAST pot perdre (configuracions incorrectes, vulnerabilitats en dependències binàries) però requereix un entorn de test funcional.
El secrets scanning en commits impedeix que secrets arribin al repositori. Eines open source com truffleHog o Gitleaks escanegen cada commit buscant patrons que semblen API keys, contrasenyes, o claus privades. Les principals plataformes de gestió de codi ofereixen secret scanning integrat. Alguns s’integren com a pre-commit hooks, bloquejant el commit localment. Altres escanegen el repositori sencer periòdicament, detectant secrets que podrien haver-se esmunyit.
L’anàlisi de dependències s’ha discutit anteriorment, però val la pena destacar que hauria d’executar-se en cada build, no només periòdicament. Una dependència segura ahir pot tenir una vulnerabilitat publicada avui.
El container scanning verifica imatges abans de desplegar-les. Fins i tot si vas escannejar la imatge quan es va construir, val la pena tornar-ho a fer abans del deploy per detectar vulnerabilitats descobertes mentrestant.
Logging i Monitoring
Els logs de seguretat són essencials per detectar i investigar incidents. Haurien de capturar tots els esdeveniments rellevants per a seguretat: intents d’autenticació (especialment fallits), canvis en permisos o configuració, accés a recursos sensibles, i errors que podrien indicar atacs.
Els intents d’autenticació fallits poden indicar password spraying o credential stuffing. Múltiples fallades des de la mateixa IP o contra el mateix compte són sospitosos. Tanmateix, cal equilibri: bloquejar accounts després de poques fallades facilita denial of service contra usuaris legítims.
Els canvis de permisos són crítics. Si un usuari regular adquireix de sobte permisos d’admin, podria ser escalació de privilegis. Els canvis en configuració de seguretat (security groups, firewall rules) haurien de triggerejar alertes per a review.
L’accés a recursos sensibles hauria de loguejar-se amb detall suficient per auditoria: qui va accedir què i quan. Això és sovint un requisit de compliance (GDPR, HIPAA) i essencial per investigar data breaches.
La centralització de logs és necessària en arquitectures distribuïdes. Stacks com ELK (Elasticsearch, Logstash, Kibana) o Grafana Loki permeten cerques potents i visualitzen tendències, convertint terabytes de logs crus en insights accionables.
La detecció d’anomalies utilitza machine learning per aprendre el comportament normal i alertar sobre desviacions. Un usuari que normalment accedeix des d’Espanya i de sobte accedeix des de Xina és sospitós. Un servidor que normalment serveix 100 requests/minut i de sobte en serveix 10.000 podria estar sota atac.
La retenció de logs equilibra costos d’emmagatzematge amb necessitats d’auditoria i investigació. Les regulacions sovint requereixen períodes mínims de retenció. Una estratègia comuna és mantenir logs detallats per 30-90 dies en hot storage per investigació ràpida, després arxivar-logs en cold storage més econòmic per al període de retenció complet.
Gestió Segura d’Errors
La tensió fonamental dels missatges d’error és que el detall que ajuda un desenvolupador a depurar és exactament el que ajuda un atacant a explotar el sistema. La solució és una separació estricta: el detall complet va als logs del servidor; l’usuari rep un missatge genèric més un ID de correlació que li permet contactar suport sense exposar informació interna.
Els stack traces en les respostes revelen noms de classes, versions de llibreries, camins de fitxers i números de línia — tot el que un atacant necessita per construir un exploit dirigit. Això s’aplica tant a respostes JSON d’API com a pàgines d’error HTML. Els frameworks solen mostrar-los per defecte en mode de desenvolupament; és essencial desactivar-los explícitament en producció.
// Insegur: exposa informació interna
{
"error": "NullPointerException at com.example.UserService.findById(UserService.java:143)"
}
// Segur: missatge genèric + ID de correlació per als logs
{
"error": "S'ha produït un error intern. Si us plau, contacteu suport.",
"correlacioId": "f3a2-8b91-4dc0"
}
L’ID de correlació és un UUID generat per petició que s’adjunta a totes les entrades de log relacionades. Quan un usuari informa d’un problema, el seu ID de correlació permet localitzar immediatament el context complet als logs del servidor, sense que l’usuari hagi vist res d’allò.
Els missatges d’autenticació mereixen atenció especial: mai s’ha de diferenciar entre “usuari no trobat” i “contrasenya incorrecta”. Tots dos han de retornar exactament el mateix missatge ("Credencials incorrectes") i el mateix temps de resposta (per evitar atacs de temporització). Missatges distints permeten als atacants enumerar quins usuaris existeixen al sistema.
Pel que fa a les categories d’error HTTP: els errors 4xx (error del client) poden acompanyar-se d’un missatge sanititzat que ajudi l’usuari a corregir la petició. Els errors 5xx (error del servidor) han de retornar sempre un missatge genèric i un ID de correlació, mai detalls de la implementació. Retornar un codi 200 amb un cos d’error és un antipatró que trenca el monitoring i hauria d’evitar-se.
La gestió d’excepcions s’ha de centralitzar a la capa de frontera (controladors, gestors de peticions): és aquí on es captura l’excepció, es registra amb tot el context als logs, i es tradueix a una resposta segura. Deixar que les excepcions es propaguin fins al framework sense capturar-les és arriscat, perquè el comportament per defecte dels frameworks sovint inclou informació diagnòstica. Totes les pàgines d’error per defecte del framework han d’estar substituïdes per pàgines personalitzades en entorns de producció.
Cloud Security
Els cloud providers ofereixen infraestructura segura, però la seguretat de les aplicacions és responsabilitat compartida: el provider assegura la infraestructura física, hypervisors i xarxa base; el client és responsable del sistema operatiu, aplicacions, dades i configuració.
IAM (Identity and Access Management) és l’àrea més rellevant per a un desenvolupador. El principi de mínim privilegi s’aplica estrictament: cada usuari, servei o rol només hauria de tenir els permisos mínims necessaris. Els rols són preferibles a les credencials estàtiques per a serveis: una instància o funció serverless rep un rol IAM i els SDKs obtenen automàticament credencials temporals, eliminant el risc de credencials filtrades. MFA hauria de ser obligatori per a tots els usuaris humans amb accés a la consola.
El xifratge hauria d’estar activat a tot arreu. En repòs, els serveis de xifratge gestionats del provider gestionen la rotació de claus i l’auditoria d’accés. En trànsit, TLS entre tots els serveis. Molts serveis cloud l’ofereixen per defecte, però cal verificar-ho i configurar-ho explícitament.
Seguretat en Aplicacions Mòbils
Secure Storage
Les aplicacions mòbils no poden confiar en que l’entorn és segur. Els dispositius poden ser rooted/jailbroken, permetent accés directe al filesystem. Les aplicacions necessiten protegir dades sensibles fins i tot en aquests escenaris.
En iOS, els Keychain Services són el mecanisme recomanat per emmagatzemar secrets. Està integrat amb el Secure Enclave del dispositiu, un coprocessador criptogràfic dedicat. Les dades al Keychain estan xifrades amb claus que mai surten del Secure Enclave. L’accés pot requerir autenticació biomètrica, assegurant que només el propietari del dispositiu pot accedir als secrets.
En Android, EncryptedSharedPreferences i Android Keystore proporcionen funcionalitat similar. EncryptedSharedPreferences xifra automàticament les dades utilitzant claus del Android Keystore. El Keystore hardware-backed (disponible en dispositius moderns) genera i emmagatzema claus en hardware dedicat, fent-les inaccessibles fins i tot amb root access.
Per a aplicacions cross-platform, llibreries com flutter_secure_storage o react-native-keychain proporcionen abstraccions sobre aquests mecanismes natius. Utilitzen Keychain en iOS i Keystore en Android, oferint una API consistent.
El que mai s’hauria de fer és emmagatzemar secrets en SharedPreferences normal (Android) o UserDefaults (iOS). Aquests arxius són plaintext (o fàcilment desxifrables) i accessibles amb root/jailbreak. Tampoc hauria d’emmagatzemar-se informació sensible en la base de dades SQLite de l’app sense xifratge, ja que aquests arxius són igualment accessibles.
Certificate Pinning
En connexions HTTPS normals, el dispositiu confia en certificats signats per qualsevol CA reconeguda. Si un atacant pot comprometre una CA o afegir una CA pròpia al trust store del dispositiu (comú en dispositius corporatius), poden executar man-in-the-middle attacks interceptant tràfic HTTPS.
Certificate pinning hardcodeja quins certificats o claus públiques són vàlides per al teu servidor. Durant el handshake TLS, l’app compara el certificat rebut amb els pins hardcodejats. Si no coincideix, la connexió és rebutjada, fins i tot si el certificat està correctament signat.
Hi ha dues aproximacions: pinar el leaf certificate (el certificat específic del teu servidor) o pinar la clau pública intermèdia. Pinar el leaf requereix actualitzar l’app cada vegada que renovis el certificat (normalment cada 3 mesos amb Let’s Encrypt). Pinar la clau intermèdia dona més flexibilitat ja que pots rotar leaf certificates mentre mantinguis la mateixa CA.
És crucial incloure backup pins. Si el teu certificat principal es compromet o necessites rotar-lo d’emergència, un backup pin permet la transició sense deixar usuaris amb versions antigues de l’app sense poder connectar. Aquests backups poden ser claus de CAs alternatives o certificats futurs que planeje utilitzar.
Cal saber que el certificate pinning és una tècnica en declivi: Android va eliminar el suport natiu a partir d’Android 7 a favor de la Network Security Configuration, i la indústria tendeix cap a Certificate Transparency (logs públics de tots els certificats emesos) com a alternativa menys fràgil. El risc amb pinning és que un error pot bloquejar tots els usuaris. Si el certificat expira i no has actualitzat l’app a temps, o si perds accés a les claus privades i has de canviar completament de CA, usuaris amb l’app instal·lada no podran actualitzar (ja que no poden connectar per descarregar l’update). Per això, algunes organitzacions eviten pinning o utilitzen periods de grace.
Code Obfuscation
El codi de les aplicacions mòbils resideix al dispositiu de l’usuari. Un atacant amb temps i eines pot fer reverse engineering per entendre com funciona l’app, extreure secrets hardcodejats, o trobar vulnerabilitats.
Eines d’ofuscació per Android com R8 (successor de ProGuard, integrat al build system d’Android per defecte) minifiquen i ofusquen el codi Java/Kotlin. Renombren classes, mètodes i variables a noms sense sentit, eliminen codi no utilitzat, i apliquen optimitzacions que fan el bytecode més difícil de seguir. Tanmateix, el bytecode de la JVM és relativament fàcil de decompile, així que ofuscació no és una protecció completa.
En iOS, el compilador genera codi natiu (no bytecode interpretable), fent-lo inherentment més difícil de fer reverse engineering. Tanmateix, eines de desassemblat encara poden analitzar binaris i, amb esforç, reconstruir la lògica.
És important entendre que l’ofuscació només fa el reverse engineering més difícil i costós, no impossible. No és una substitució per a disseny segur. Secrets crítics (claus d’API, encryption keys) mai haurien d’estar hardcodejats en l’app, independentment de l’ofuscació. Aquests han de venir del backend o generar-se dinàmicament.
L’ofuscació també pot complicar el debugging de crashes en producció. Els stack traces contenen noms ofuscats, fent difícil identificar on va ocórrer l’error. Les mapping files (que relacionen noms ofuscats amb originals) han de mantenir-se segures i disponibles per desofuscar crash reports.
Biometric Authentication
L’autenticació biomètrica proporciona seguretat i usabilitat: és més difícil robar una empremta o cara que una contrasenya, i és més convenient per a l’usuari.
En iOS, el LocalAuthentication framework proporciona una API senzilla per sol·licitar autenticació. L’app mai accedeix a les dades biomètriques reals; simplement pregunta al sistema “aquest usuari s’ha autenticat correctament?” i rep un sí o no. Les dades biomètriques romanen al Secure Enclave i mai són accessibles a aplicacions.
Android ofereix la BiometricPrompt API, que proporciona una experiència consistent independentment del mètode biomètric disponible (empremta, face unlock, iris scan). Com iOS, les dades biomètriques estan protegides pel sistema i no accessibles a l’app.
Els fallbacks són essencials. No tots els dispositius tenen capacitats biomètriques, i les biomètriques poden fallar temporalment (mans mullades per empremtes, màscares per reconeixement facial). L’app hauria de permetre autenticació amb PIN o patró quan la biometria no està disponible o falla.
La biometria no hauria de ser l’única capa de seguretat per operacions crítiques. Per transaccions financeres o canvis de configuració sensibles, combinar biometria amb un segon factor (com OTP) proporciona defensa en profunditat.
Desenvolupament d’interfícies
Disseny i implementació
- Aplicacions web
- Patrons de disseny
- JavaScript i APIs
- Paradigma declaratiu
- Toolchain
- Fonaments React
- Disseny React
Referències
- JavaScript modern
- JavaScript, MDN
- Introduction to web APIs, MDN
- Go Make Things
- The Modern JavaScript Tutorial
- Client-Side Web Development
- JavaScript and React Patterns
- Web Applications 101
- https://node.green/
- React Fiber Architecture
Aplicacions web
Una aplicació web és un programari que implica un client i un servidor que es comuniquen utilitzant la xarxa i el seu protocol HTTP.
- El client és un navegador web, que sap com renderitzar documents basats en els estàndards web HTML (estructura), CSS (estil) i JavaScript (lògica).
- El servidor permet rebre peticions HTTP de diferents tipus (GET, POST, PUT, DELETE…) per a retornar, crear, modificar o esborrar continguts.
Hi ha diferents models d’aplicació web que es distingeixen en funció de qui, com i quan genera els documents o continguts que renderitzarà el navegador per a l’usuari.
Història
Inicialment, els continguts eren només estàtics. Hi ha servidors web que serveixen HTML i CSS amb HTTP GET. L’adreça del navegador és un HTTP GET cap el servidor, que serveix continguts estàtics. Aquests continguts els ha creat prèviament un dissenyador utilitzant eines de disseny.
L’aparició de PHP revoluciona la web. Es tracta d’un llenguatge que permet rebre peticions HTTP POST, accedir a bases de dades i generar documents HTML de forma dinàmica. Per tant, els documents els genera el servidor dinàmicament en funció de les peticions del navegador utilitzant motors de plantilles. Se’n diuen Multi-Page Applications (MPA), i van apareixen nous frameworks (Servlets, ASP.NET, Laravel, Django, Spring).
També el navegador evoluciona, amb JavaScript, que permet manipular el DOM i fer interaccions dinàmiques, així com Ajax, que permet fer peticions al servidor sense recarregar la pàgina. Els navegadors són cada cop més compatibles, i es desenvolupa un motor JavaScript en el servidor, Node.js. Podem comunicar-nos amb el servidor amb altres tipus de continguts, com per exemple JSON. També tenim JQuery, una llibreria que facilita la manipulació del DOM.
És el preàmbul de les Single-Page Applications (SPA).
Single-Page Application
Una SPA encapsula la majoria de la lògica de l’aplicació al client mitjançant JavaScript, i sap com generar els documents HTML i CSS. El navegador obté aquesta lògica mitjançant una petició de documents estàtics (HTML amb JavaScript), i esdevé una aplicació autònoma. De fet, intercepta el routing de la URL a l’adreça del navegador, que no produeix necessàriament comunicació amb el servidor.
Només accedirà a un servidor si necessita interactuar amb altres recursos externs dins d’una arquitectura centralitzada que comparteix amb altres usuaris o aplicacions. D’aquesta arquitectura se’n diu full-stack web, i té dues aplicacions: la del client (front-end) i la del servidor (back-end), comunicades amb un protocol sobre HTTP i un format de dades, com JSON.
Tenim diverses solucions al client i al servidor, que poden ser intercanviables si es respecta el protocol:
- Al client hi ha diverses llibreries o frameworks, com Angular, React o Vue.
- Al servidor es poden utilitzar frameworks MPA o bé de nous, com Node.js (Express.js).
Patrons de disseny
En parlarem dels patrons bàsics en els quals es basa el disseny d’interfícies.
Vista d’arbre
La vista d’arbre és un patró en el qual es basen moltes eines. La idea essencial és que hi ha una estructura en forma d’arbre de vistes (també nodes o elements). Cada vista pot contenir altres vistes (contenidors de vistes). Normalment, si una vista no es mostra tampoc ho fan les vistes contingudes.
- Sortida: les vistes són responsables de la seva visualització, i quan es muten es tornen a redibuixar.
- Entrada: les vistes tenen gestors d’entrades en funció de la interacció del teclat o del ratolí.
- Maquetació: l’arbre és responsable de la disposició de les vistes, generant el seu bounding box, que permet la seva maquetació en funció d’algorismes.
Listeners
Els listeners, també anomenats gestors d’esdeveniments, subscriptors o observadors, gestionen els esdeveniments d’entrada. Permeten assignar un gestor quan un esdeveniment concret passa.
Model-View
La separació d’interessos (Separation of Concerns: SoC) ens diu que és millor que àrees de diferents funcionalitats tinguin poc solapament. La primera separació amb sentit seria que la sortida la gestionin les vistes i l’entrada els listeners. Però encara ens faltaria afegir l’aplicació en sí.
Per a interpretar els patrons a continuació utilitzarem tres tipus d’interaccions:
- Comandes: són peticions per a realitzar canvis. No esperen cap resposta.
- Consultes (queries): són peticions per a obtenir valors de l’estat. No realitzen cap canvi.
- Notificacions: són notificacions d’esdeveniments que han passat.
En relació a comandes i consultes, és important comentar el concepte de Command-Query Separation. El que afirma és que cada mètode hauria de ser una ordre que realitzi una acció o una consulta que retorni dades al client, però no totes dues.
El criteris generals que segueixen tots els patrons són:
- El Model no coneix (no depèn de) cap altre component del patró.
- La View és l’únic component que fa referència a aspectes visuals.
MVC
El primer patró que es va utilitzar és MVC (Model-View-Controller).
Aquest patró utilitza Model, View i Controller.
- View: rep notificacions de canvis al Model i notifica el Controller d’esdeveniments de la UI.
- Controller: rep notificacions d’esdeveniments a la View i envia comandes al Model i la View.
- Model: notifica la View i rep consultes seves. També rep comandes i consultes del Controller.

El flux és:
- View (esdeveniment)
- Controller (comanda a Model)
- Model (esdeveniment)
- La View s’actualitza
MVP
Per tal de fer més testable per unitats cada part, apareix el patró MVP, on el Controller es substitueix pel Presenter. Aquest patró fa que totes les interaccions passin pel Presenter, i la View i el Model mai es comuniquen.

El flux és:
- View (esdeveniment)
- Presenter (comanda a Model)
- Model (esdeveniment)
- Presenter (comanda a View)
- La View s’actualitza
MVVM
Aquest patró és similar al MVP, però substitueix el Presenter pel ViewModel. Utilitza la idea de data binding, on els canvis de les dades es transmeten fins a la View a partir del Model mitjançant el ViewModel.

El flux és:
- View (commanda a ViewModel)
- ViewModel (comanda a Model)
- Model (esdeveniment)
- ViewModel (esdeveniment)
- La View s’actualitza
Construcció de la vista
A l’hora de construir l’arbre de vistes tenim dos paradigmes:
- Procedural: el codi indica, pas a pas, com arribar a l’estat desitjat de l’arbre de vistes.
- Declaratiu: el codi representa directament l’arbre de vistes en funció de l’estat actual.
El paradigma procedural consisteix en escriure la seqüència de passos necessària per a convertir l’arbre de vistes anterior al nou, reflectint el nou estat de l’aplicació.
En canvi, el paradigma declaratiu necessita un codi que directament reprodueixi l’arbre de vistes a partir de l’estat. Algunes eines permeten identificar quin subconjunt de l’arbre de vistes cal tornar a generar, sempre que puguem identificar quines vistes s’han vist afectades per la mutació de l’estat.
JavaScript i APIs
Per a poder desenvolupar al front-end, cal tenir un coneixement de JavaScript avançat basat en l’estàndard ECMAScript ES6 (2015) i que s’utilitza al front-end i a l’entorn de desenvolupament associat.
Veure la pàgina de JavaScript modern per a entendre les funcionalitats base del llenguatge.
Al marge del llenguatge, tenim una sèrie d’APIs que permeten interactuar amb el navegador anomenades Web API:
- Manipulació DOM (DOM API), que inclou, entre d’altres:
- Accés i control d’elements HTML
- Manipulació de dades de formularis
- Accés a la història del navegador
- Scripts en background (Web Workers API)
- Websockets i Server-sent events
- Obtenció de dades d’un servidor (Fetch API)
- Manipulació de gràfics (Canvas i WebGL) i interacció amb àudio i vídeo
- APIs del dispositiu, per exemple, geolocalització
- Emmagatzematge de dades al dispositiu (Web Storage API)
Cal dir que Node.js, un runtime JavaScript standalone, és principalment compatible amb l’especificació ES6 (veure node.green), però no amb les APIs del navegador.
Esdeveniments
Podem associar un esdeveniment del DOM a una crida d’una funció amb element.addEventListener(event, fn). Podem esborrar el listener amb element.removeEventListener(event, fn).
La funció listener pot tenir un paràmetre, l’esdeveniment. Aquest objecte té la propietat target, que fa referència a l’objecte que origina l’esdeveniment, i type, el tipus d’esdeveniment, entre d’altres.
Manipulació DOM
Alguns mètodes importants que permeten manipular el DOM:
document.querySelector(selector),document.querySelectorAll(selector). Versions clàssiques:getElementById(id),getElementsByTagName(name)…document.createElement(tag),element.appendChild(child),element.removeChild(child),element.remove().
Fetch API
El mètode fetch(resource, options) és la forma estàndard d’obtenir dades de forma asíncrona. Retorna una promesa.
Paradigma declaratiu
En la programació imperativa, el canvi està associat al flux d’execució i l’operador assignació. És una definició operacional.
La programació declarativa es basa en la definició conceptual, es defineixen relacions atemporals.
La programació reactiva es basa en el paradigma declaratiu: les nostres sortides es descriuen declarativament en funció de les nostres entrades. Això les permetrà reaccionar als canvis que es produiran a les entrades al llarg del temps.
Si utilitzem programació basada en esdeveniments, podem associar un listener als canvis (esdeveniments) produïts a les entrades. Llavors, es poden modificar les sortides. Reactivament, canviem el concepte per una relació “el seu valor depèn de” de les sortides cap a les entrades. Conceptualment, podem imaginar que una sortida és listener de totes les entrades de què depèn, i que actualitza el seu valor cada cop que es crida.
El funcionament reactiu executa un programa es produeix en dues fases:
- Inicialització: construir un graf dirigit de dependències
- Execució: els valors entren i produeixen canvis a les sortides És similar a com funciona una GUI: primer es construeixen els widgets i després el bucle d’esdeveniment els processa.
Gestió de l’estat
La UI (vista) és el resultat de dibuixar una representació de les dades. La gestió de l’estat inclou com representar aquestes dades i gestionar els seus canvis. La relació entre UI i estat és:
UI = fn(estat)
D’això se’n diu UI basada en l’estat. Per explicar com es produeixen els canvis que un usuari produeix sobre l’estat, cal introduir el concepte d’acció:
En aquest esquema es diu que l’estat és un observable, i que l’UI és un observador dels canvis que es produeixen en l’estat. Els observadors es subscriuen als observables, i seran notificats per ells quan hi hagi canvis.
Per tant, l’UI no canvia directament l’estat, li cal passar un missatge (acció) que encapsula el canvi d’estat. Un canvi d’estat fa que tots els seus observadors siguin notificats. L’UI és el principal subscriptor, i quan se li notifica d’un canvi d’estat (observables) torna a renderitzar-se per a reflectir-ho.
Efectes secundaris
Algunes operacions no tenen un efecte directe sobre la UI. Per exemple: obtenir dades d’un servidor, guardar-les, executar alguna tasca periòdica, etc. Aquestes són operacions externes no associades a l’UI, i les diem efectes secundaris o side effects.
Web reactiva
Les interfícies (o sortides) reaccionen als canvis que es produeixen a les seves dependències (o entrades). Són observadors. El paradigma reactiu ens permet oblidar-nos de com i quan es tornarà a redibuixar la UI. D’això s’encarrega la llibreria, que detecta els canvis a realitzar al DOM i els aplica.
La relació entre entrada i sortida és declarativa, i s’implementa de diferents formes segons la llibreria o framework. Habitualment tenim un cert estat (entrades) i un markup (sortides) que el referencia.
Les operacions bàsiques essencials d’un framework reactiu són:
- Definició d’estats (observables).
- Definició d’UI (observador) a partir dels estats (observables).
- Definició de side-effect handlers (observador).
- Modificació d’un estat a partir d’un esdeveniment UI o un side-effect.
- Creació d’un observable computat a partir d’altres observables (optimització).
Toolchain
L’entorn de desenvolupament dels frameworks JavaScript requereix d’un conjunt d’eines (toolchain) per a realitzar diferents funcions:
- Gestor de paquets: permeten instal.lar i utilitzar paquets de tercers tant en temps de desenvolupament com en temps d’execució. Exemples: npm, yarn.
- Bundler: permet desenvolupar modularment el codi i després optimitzar-lo per a carregar ràpidament en temps d’execució. Exemples: webpack, parcel, vite.
- Compiladors: permeten transformar el codi font (transpile/polyfill) des de JavaScript modern (cada cop menys necessari), Typescript o altres DSL (syntax sugar) a JavaScript que pot executar-se als navegadors. Exemples: babel.
- Linters: analitzador estàtic de codi que permet detectar errors. Exemple: eslint.
- Prettiers: formatadors de codi. Exemple: prettier.
Totes aquestes eines són integrables des de diferents editors de codi, com per exemple VSCode o Webstorm.
Fonaments React
React és un framework basat en la programació declarativa de components. Cada component declara com s’ha de renderitzar en funció de les propietats d’entrada i el seu estat:
(props, state) => view
La vista només es torna a renderitzar si canvien les propietats o l’estat.
Els components pare es comuniquen amb els fills passant propietats, i els fills poden generar esdeveniments cap als pares mitjançant callbacks.
El procés de renderitzar components es realitza sobre un DOM virtual, detectant quins canvis s’han produït a l’arbre de nodes respecte el DOM anterior. Només els canvis s’enviaran al DAM real, en un procés anomenat reconciliació.
Hi ha dues formes d’escriure de components:
- basats en classes (tradicional): s’utilitzen classes JavaScript que estenen React.Component amb mètodes del cicle de vida. Cal, almenys, definir el mètode render(), una funció pura que retorna el contingut a renderitzar.
- basats en funcions (modern): són funcions JavaScript amb un paràmetre, les propietats, que retornen el contingut a renderitzar. Utilitzen el concepte de hook, unes crides al framework que permeten gestionar l’estat o els efectes secundaris, per exemple.
Els components responen a tres esdeveniments:
- Muntatge (mount): quan el component es crea i s’afegeix al DOM.
- Actualització (update): quan el component s’actualitza en modificar-se una propietat o estat.
- Desmuntatge (unmount): quan el component s’esborrar del DOM.
El flux de React és el següent:
- Fase de renderitzat
- Es construeixen els components a muntar (1)
- Es renderitzen a VDOM els nous components i aquells que calgui actualitzar (1, 2)
- Fase de commit
- S’actualitza el DOM (real)
- El navegador pinta el DOM actualitzat
- Els components actualitzats i desmuntats (2, 3) fan el cleanup d’efectes
- Els components nous i actualitzats criden els efectes (1, 2)
Toolchain
El toolchain React és un conjunt d’eines que permeten desenvolupar aplicacions React. Inclou un servidor de desenvolupament, un compilador de JavaScript (Babel), un gestor de paquets (npm o yarn) i un empaquetador de mòduls (Webpack o Vite).
Després que React va deprecar el create react app, el toolchain oficial és Vite. Vite és un empaquetador de mòduls que permet un desenvolupament més ràpid i eficient. Utilitza una arquitectura basada en mòduls ES i ofereix una experiència de desenvolupament més ràpida i fluida.
Per crear una aplicació React amb Vite cal executar la comanda següent (segons es volgui JavaScript o TypeScript):
# React + JavaScript
$ npm create vite@latest my-app --template react`
# React + TypeScript
$ npm create vite@latest my-app --template react-ts
JSX i components
React utilitza una sintaxi estesa de JavaScript anomenada JSX. Un compilador del toolchain (Babel) s’encarrega de convertir-ho a JavaScript. Està basada en HTML, però permet codi JavaScript dins de claus {}.
Permet definir elements React, que són objectes creats amb React.createElement(). Els elements poden ser de tipus HTML o bé definits pel desenvolupador.
Conceptualment, els components són funcions de JavaScript. Accepten entrades arbitràries (anomenades props) i retornen elements de React que descriuen el que hauria d’aparèixer a la pantalla. Aquestes propietats són de només lectura, i poden incloure callbacks per a gestionar esdeveniments.
Flux de dades
Les dades de React només viatgen des del pare cap els fills (one way data flow), i ho fan mitjançant les props. Les props són un objecte immutable, mentre els estats són mutables. Però com les props són immutables, els canvis de l’estat als fills no afecten els pares.
function Child({ propName }) {
return (<h1>Hello {propName}!</h1>);
}
function Parent() {
return (<Child propName={"React"} />);
}
function Child({ propName }: { propName: string }) {
return (<h1>Hello {propName}!</h1>);
}
function Parent() {
return (<Child propName={"React"} />);
}
Com que un fill no pot modificar l’estat d’un pare el que fem és aixecar l’estat (lifting state up). Això permet compartir-lo i accedir a aquells components que el necessiten.
El flux invers és possible utilitzant esdeveniments enviats des dels fills als pares.
function Child({ message, callBack }) {
return (
<button onClick={() => callBack("message from child!")}
>{message}</button>);
}
function Parent() {
const [message, setMessage] = useState('no message');
return (
<Child message={message}
callBack={propValue => setMessage(propValue)} />);
}
function Child({ message, callBack }: { message: string, callBack: () => void }) {
return (
<button onClick={() => callBack("message from child!")}
>{message}</button>);
}
function Parent() {
const [message, setMessage] = useState<string>('no message');
return (
<Child message={message}
callBack={propValue => setMessage(propValue)} />);
}
Hooks
React modern utilitza els components basats en funcions. Un component és una funció amb props d’entrada i el contingut a mostrar de sortida, i utilitza els hooks per a definir el seu comportament.
Els hooks permeten implementar les operacions de React:
- Definició d’estats amb useState.
- Definició d’UI a partir dels estats amb el JSX retornat.
- Definició de side-effect handlers amb useEffect.
- Modificació d’un estat amb setState.
- Creació d’un observable computat a partir d’altres observables amb useMemo.
React permet definir custom hooks per part de l’usuari. Bàsicament, es tracta de lògica amb estat reutilitzable on no hi ha JSX. S’implementen mitjançant una funció anomenada useXXX que pot contenir altres hooks i, habitualment, retornen un objecte o una tupla.
import { useState, useEffect } from 'react';
function useDelayedMessage(delay = 1000) {
const [message, setMessage] = useState("Waiting...");
useEffect(() => {
const timer = setTimeout(() => {
setMessage("Hello, world!");
}, delay);
// Cleanup the timer if the component unmounts or delay changes
return () => clearTimeout(timer);
}, [delay]);
return message;
}
import { useState, useEffect } from 'react';
function useDelayedMessage(delay: number = 1000): string {
const [message, setMessage] = useState<string>("Waiting...");
useEffect(() => {
const timer = setTimeout(() => {
setMessage("Hello, world!");
}, delay);
// Cleanup the timer if the component unmounts or delay changes
return () => clearTimeout(timer);
}, [delay]);
return message;
}
Disseny React
- 1 Descomposició
- 2 Versió estàtica
- 3 Estat mínim i suficient
- 4 Localització de l’estat
- 5 Flux invers de dades
- 6 Extreure lògica dels components
Imaginem que tenim una maqueta de la interfície. Els passos habituals per a dissenyar una aplicació React serien:
- Descompondre la interfície en una jerarquia de components.
- Construir una versió estàtica en React.
- Trobar la mínima representació possible de l’estat.
- Identificar on ha de situar-se l’estat dins de la jerarquia.
- Afegir flux invers de dades.
- Extreure lògica dels components.
1 Descomposició
Identifica les responsabilitats dins del disseny i separa-les en components.
2 Versió estàtica
Començar creant una versió no interactiva. Cal decidir quines props passen de dalt cap a baix de la jerarquia, però sense utilitzar estat. Es diu flux d’un sol sentit.
3 Estat mínim i suficient
La interfície d’usuari es fa interactiva gràcies a la possibilitat de modificar les dades del nostre model. Ho podem fer utilitzant estat: el mínim conjunt de dades que la nostra aplicació ha de recordar.
Identifica les dades que són estat:
- canvien al llarg del temps,
- no arriben per les props,
- no són computables a partir d’un altre estat o les props.
4 Localització de l’estat
Cal identificar quin component modifica l’estat i per tant li pertany. Es pot fer en tres passos:
- Identifica els components que utilitzen l’estat
- Trobar el pare comú més proper de tots dins la jerarquia
- Decideix on ha de localitzar-se. Habitualment és al pare comú, tot i que podria ser qualsevol ancestre d’aquest pare.
5 Flux invers de dades
Si volem que un component fill pugui modificar l’estat d’un component pare, cal que el pare passi un callback mitjançant les props que el fill pugui cridar per a realitzar el flux invers de dades.
6 Extreure lògica dels components
Els custom hooks són una forma d’extreure lògica dels components amb estat. Si ens trobem que utilitzem diversos hooks de React, amb diversos estats (useState) i possiblement effectes (useEffect) o altres, es pot simplificar i encapsular en un custom hook. Això simplificarà el codi i, encara que no és un requisit, permetrà la seva reutilització en altres components.
Usabilitat i informes
Referències
- 10 Usability Heuristics for User Interface Design
- The Beginner’s Guide to Information Architecture in UX
- Designing for Web Accessibility
User Experience (UX)
La UX, o User eXperience, és com percep l’usuari final l’ús del teu producte, sistema o servei. Inclou emocions, creences, preferències, percepcions, respostes físiques i psicològiques. I tot això, tant abans, durant com després de l’ús.
Una de les teories que intenten explicar la UX és la dels tres cercles de l’arquitectura de la informació. Segons aquesta, els tres components a tenir en compte per a dissenyar un producte digital són:
- El context: el negoci o la missió.
- Els usuaris: les necessitats.
- El contingut: la solució tècnica.
Els elements de la UX són (segons Garrett), de més abstracte a més concret:
- Els objectius del generals del servei: si és de negoci, creatiu, social, etc.
- Les necessitats de l’usuari, relacionat amb el seu origen i una segmentació.
- Les especificacions funcionals per a complir les necessitats de l’usuari.
- El disseny de la interacció: ha de facilitar les tasques, i defineix com interactua l’usuari amb les funcionalitats.
- El disseny de la informació: com es presenta per a facilitar la seva comprensió.
- El disseny de la interfície que facilita les interaccions.
- El disseny visual (look-and-feel).
Les facetes de la UX que ha de reflectir el nostre producte o servei (segons Morville) són:
- Útil: ha d’omplir una necessitat dels usuaris.
- Usable: ha de ser simple i fàcil d’utilitzar, trobar-ho familiar, amb poc aprenentatge.
- Desitjable: ha de provocar apreciació i emocions favorables amb la imatge, identitat i marca.
- Localitzable: ha de ser fàcil trobar tot allò que es busca.
- Accessible: hem de proporcionar accessibilitat a persones amb discapacitats.
- Confiable: el disseny també influeix la credibilitat i confiança dels usuaris.
- Valuable: si és amb ànim de lucre, ha de contribuir a l’objectiu i millorar la satisfacció de client. Si no ho és, ha de avançar la seva missió.
Les tasques a realitzar associades a l’UX són variades, i generen diferents perfils de dissenyadors:
- Un dissenyador UX ha de fer recerca, identificar necessitats i crear fluxes de tasques.
- El dissenyador d’UI s’encarrega de la part cosmètica: la tipografia, els colors, l’espaiat, les icones, etc.
Usabilitat
- Visibilitat de l’estat
- Reflex del món real
- Control i llibertat
- Consistència i estàndards
- Prevenció d’errors
- Reconèixer millor que recordar
- Flexibilitat i eficiència
- Disseny minimalista i estètic
- Identificació d’errors
- Ajuda i documentació
La usabilitat és la capacitat d’un sistema de proporcionar les condicions perquè els seus usuaris puguin realitzar les seves tasques de forma segura, efectiva i eficient, i que ho facin gaudint l’experiència. COm hem vist, es tracta d’una de les facetes de la UX.
Aquests són 10 principis heurístics per a dissenyar interfícies usables (segons Nielsen).
- Visibilitat de l’estat del sistema. Cal compartir l’estat, i no actuar sense informar, donant feedback tan aviat com sigui possible.
- Reflex del món real. No hi ha d’haver vocabulari desconegut, cal reflectir la terminologia més familiar.
- Control i llibertat de l’usuari. Permetre desfer i refer, saber com cancel.lar i sortir.
- Consistència i estàndards. Interna, al producte i a una família, i externa, amb els estàndards de la indústria.
- Prevenció d’errors. Evitar els errors en situacions proclius, sempre comprovar-los i informar si cal alguna acció.
- Reconèixer millor que recordar. Reduir la quantitat d’informació que cal recordar, ajudar contextualment millor que fer tutorials.
- Flexibilitat i eficiència. Proporcionar dreceres, personalització i customització per a permetre diferents formes de portar a terme accions.
- Disseny minimalista i estètic. Centrar-se en els qüestions essencials, traient aspectes innecessaris i prioritzant els objectius principals.
- Identificació d’errors. Els errors s’han d’expressar en un llenguatge planer que indiqui el problema de forma precisa i ofereixi una solució constructiva.
- Ajuda i documentació. Ajuda fàcil de buscar, en el context i amb passes concretes a realitzar.
Visibilitat de l’estat
Només podem canviar l’estat del sistema si sabem quin és aquest estat. Un cop tenim un objectiu, hi ha dos processos problemàtics o bretxes:
- l’avaluació: quin és l’estat actual del sistema?
- l’execució: com puc utilitzar el sistema?
Alguns consells a seguir:
- Dissenyar el sistema per a informar de l’estat per a poder actuar sobre ell, i veure com les accions modifiquen l’estat (interdependència). A més, el disseny ha de ser familiar per als usuaris. Per exemple: el comptaquilòmetres i l’accelerador.
- Donar una realimentació apropiada un cop es produeix la interacció, si la operació està en progrés i si ha acabat.
- L’estat i les possibles interaccions ha de mostrar-se de forma senzilla, convidant els usuaris a actuar.
Aquests són les opcions comunicatives que tenim:
- Indicadors: fem destacar un element per a informar l’usuari que se l’ha d’atendre. Es pot fer amb icones, variacions tipogràfiques, canvis de mida o animacions. Són contextuals, apareixen condicionalment i són passius (no cal actuar).
- Validacions: són missatges d’error sobre un problema a una entrada per part de l’usuari, que haurà d’actuar. Es pot utilitzar una icona.
- Notificacions: alerten l’usuari sobre esdeveniments generals al sistema no relacionats amb accions immediates. Poden ser contextuals o globals, i poden requerir o no accions.
Reflex del món real
Cal tenir en compte que nosaltres (dissenyadors) no som el mateix que els usuaris. Els termes que utilitzem li han de resultar familiars sense caldre consultar un diccionari.
La solució seria utilitzar objectes i activitats del món real. Si el model mental (teoria de com funciona el sistema) de l’usuari sobre el sistema s’assembla al de la realitat, li facilitem la comprensió i li diem que el coneixem i ens importa.
Control i llibertat
Alguns aspectes a considerar:
- Sempre permetre anar enrere.
- Que anar enrere sigui el que espera l’usuari.
- Que els enllaços de tancar, sortir o cancel.lar siguin visibles i estiguin on cal.
- Que es pugui cancel.lar una acció en qualsevol punt intermedi i, si cal, distingir-ho de tancar.
- Poder desfer una acció fàcilment.
- Que desfer sigui visible, i ho sigui mentre es pugui dur a terme.
Consistència i estàndards
Hi ha unes convencions que caldria respectar. Tenim internes i externes:
- Les internes dins el nostre sistema o sistemes es pot guiar mitjançant un manual d’identitat de marca.
- Les externes poden basar-se en les convencions i estàndards establerts a la indústria. Per exemple, com funciona la navegació, quin aspecte tenen les pàgines d’inici, l’aspecte dels enllaços o dels botons, com s’introdueix la informació als formularis, com funcionen les cistelles de la compra, etc.
Alguns nivells als quals podem actuar:
- El visual. Per exemple, l’hamburguesa del menú per a llocs mòbils.
- La pàgina i la distribució de botons (ordre i colors).
- La introducció de dades de forma guiada segons el seu tipus (telèfons, dates, etc.).
- El contingut, mantenint un to consistent de comunicació.
Prevenció d’errors
Tenim dos tipus d’errors:
- Relliscada: és inconscient, per un descuit, l’usuari volia fer una acció però acaba fent una altra, de forma accidental.
- Errors: és conscient, perquè el model mental de l’usuari no encaixa amb el disseny. L’acció acaba sent inapropiada per a la tasca que volem completar, perquè no hem entès com funciona el sistema.
Per a prevenir relliscades:
- Incloure limitacions a les entrades de dades.
- Oferir suggeriments i bons valors per defecte (tasques repetitives). Es poden utilitzar valors més freqüents o representatius.
- Permetre formats alternatius per a introduir certs tipus de dades.
Per a prevenir errors:
- Fes una bona visualització de l’estat.
- Recull dades dels usuaris per veure on fallen.
- Segueix les convencions de disseny.
- Permet fer una previsualització del resultat.
Per a prevenir els dos tipus d’errors:
- No obligar a recordar moltes coses.
- Confirmar abans d’accions destructives.
- Permetre el desfer.
- Avisar d’errors abans de que passin.
Reconèixer millor que recordar
Cal fer visibles i fàcilment accessibles:
- La informació necessària per a aconseguir un objectiu.
- Les funcions de la interfície, com botons, navegació i altres elements.
Per exemple:
- Tenir un historial o contingut visitat prèviament.
- La revelació progressiva amaga les funcions avançades o por habituals a espais secundaris.
- Ajuda contextual i consells en lloc de tutorials.
Flexibilitat i eficiència
No tots els usuaris tenen les mateixes necessitats. Cal:
- Tenir mètodes diferents per a acomplir la mateixa tasca, segons les preferències.
- Permetre l’usuari customitzar la interfície segons les necessitats, però deixant uns bons valors per defecte.
- També pot ser que la customització la faci el sistema, llavors es diu personalització.
- Tenir acceleradors per a usuaris avançats, que no afecten als primerencs. Per exemple, gestos o macros.
Disseny minimalista i estètic
Els aspectes estètics són importants. Les primeres impressions són importants, com també ho són la percepció visual per sobre de l’experiència. A més, reforcen la identitat de marca.
Per altra banda, el disseny minimalista demana que hi hagi tots els elements per a suportar les tasques de l’usuari, però cap altra més. Tenir elements de més pot amagar els elements necessaris. Podem reduir el soroll del disseny:
- Aprofitant patrons universals de disseny amb connotacions positives. Per exemple, certs tipus d’imatges, com paisatges.
- Acceptar que la bellesa està a l’ull de l’espectador, i que hem de considerar les persones.
Els cinc principis de disseny visual poden ajudar a assolir aquest disseny:
- Escala: utilitzar mides relatives a la importància i prioritat dins de la composició dels elements.
- Jerarquia visual: guiar l’ull dins de la pàgina perquè atengui els elements en ordre d’importància.
- Balanç: passa quan hi ha una càrrega de senyals visuals distribuïda de forma igualitària als dos costats d’un eix imaginari.
- Contrast: la juxtaposició d’elements no iguals per a mostrar elements diferents.
- Principis gestalt: la tendència a percebre diversos elements individuals com a un tot, ja que ens resulta una forma més estable.
Identificació d’errors
Segueix aquesta guia per a escriure els teus missatges d’error:
- Cal que sigui explícit, indicant que alguna cosa concreta ha fallat.
- Ha de ser fàcil de llegir per a qualsevol persona, sense codis ni abreviacions.
- Ha de ser respectuós.
- Ha de ser precís, sense vaguetats genèriques.
- Ha d’aconsellar constructivament com resoldre el problema.
A més, els errors s’han de mostrar amb tractaments visuals que permetin identificar-los i reconèixer-los. Utilitza els visuals de les convencions, com text vermell o negreta.
Ajuda i documentació
Tenim dos tipus d’ajuda: proactiva i reactiva.
L’ajuda proactiva, abans de que es produeixi un error. L’onboarding (primer cop que veiem la interfície) i els consells contextuals són d’aquest tipus. S’orienta a familiaritzar l’usuari amb l’interfície. Pot estar fora del flux de treball i s’ignoren (push) o contextuals i són útils (pull). Guia:
- Mantenir-la curta i en el punt on som per no distreure l’usuari.
- Afavorir la pull sobre la push.
- Les revelacions push haurien de ser fàcils d’ignorar.
- Ha de ser accessible des de qualsevol lloc (fins i tot quan s’han ignorat).
L’ajuda reactiva, amb materials de consulta. S’orienta a respondre preguntes, consultar problemes o ajudar a usuaris que volen ser experts. De vegades s’orienten amb un FAQ. Guia:
- Assegurar-se que la documentació és comprensiva i detallada.
- La informació més important s’ha de presentar abans.
- Considerar l’ús de gràfics i vídeos com a font secundària.
- Permetre la cerca.
- Organitzar per categories.
- Subratllar el contingut visitat freqüentment.
Accessibilitat Web
A continuació es mostren una sèrie de consells en funció de l’activitat a què es refereixen: el disseny, la creació de continguts i el desenvolupament.
Disseny accessible
- Proporcioneu un contrast suficient entre el primer pla i el fons.
- No utilitzeu només el color per transmetre informació.
- Assegureu-vos que els elements interactius siguin fàcils d’identificar: botons i enllaços.
- Proporcioneu opcions de navegació clares i coherents: capçaleres, molles de pa, mapes.
- Assegureu-vos que els elements del formulari inclouen etiquetes associades clarament.
- Proporcioneu feedback fàcilment identificable: icones, colors, etc.
- Utilitzeu els encapçalaments i l’espaiat per agrupar contingut relacionat: faciliteu la comprensió.
- Creeu dissenys per a diferents mides de visualització: responsivitat al mòbil.
- Incloeu alternatives d’imatge i mitjans al vostre disseny: textos, captions, etc.
- Proporcioneu controls per al contingut que s’inicia automàticament: als carrousels, sliders, vídeos, so de fons, etc.
Contingut accessible
- Proporcioneu títols de pàgines informatius i únics.
- Utilitzar els encapçalaments (i subencapçalaments) per transmetre significat i estructura.
- Fes que el text de l’enllaç tingui significat: evita el “clica aquí”.
- Escriu alternatives de text significatives per a imatges que tinguin una funció (no per a les decoratives).
- Crea transcripcions i subtítols per a multimèdia.
- Proporcioneu instruccions clares: a les instruccions, guies i als missatges d’error. Sense utilitzar llenguatge tècnic.
- Mantingueu el contingut clar i concís.
Desenvolupament accessible
- Associa una etiqueta a cada control de formulari (label for).
- Incloeu text alternatiu per a les imatges (alt).
- Identificar l’idioma de la pàgina i els canvis d’idioma (html lang).
- Utilitzeu les marques per transmetre significat i estructura: HTML semàntic com section, article, aside, ul, etc.
- Ajudeu els usuaris a evitar i corregir errors: indicar on hi ha el problema, explicar-ho i suggerir correccions.
- Reflecteix l’ordre de lectura en l’ordre del codi: evita que el CSS sigui qui marqui aquest ordre.
- Escriu codi que s’adapti a la tecnologia de l’usuari: responsivitat.
- Donar significat als elements interactius no estàndard: utilitza WAI-ARIA.
- Assegureu-vos que tots els elements interactius siguin accessibles amb el teclat.
- Eviteu CAPTCHA sempre que sigui possible.
Informes
La visualització de dades és el procés de representar les dades gràficament per obtenir informació i facilitar la presa de decisions. D3.js (Data-Driven Documents) és una potent biblioteca de JavaScript que permet als desenvolupadors crear visualitzacions de dades interactives i dinàmiques en aplicacions web.
Preparació i distribució
Referències
- The Practical Test Pyramid
- Testing JavaScript Applications
- Mocks Aren’t Stubs
- Jest Getting Started
- Vitest Getting Started
- Testing React Apps
- React Testing Library Cheatsheet
- Testing Overview
Proves
Tenir codi llest per a producció requereix fer proves. Com que no podem tenir milers de testers, sorgeix la necessitat d’automatitzar-les. Un procés que va de la mà de les pràctiques de desenvolupament àgil i el CD (Continuous delivery).
La piràmide de les proves
La piràmide de les proves és una metàfora visual que descriu les capes de les proves:
- Proves unitàries: validació dels blocs més atòmics del software, les funcions.
- Proves d’integració: validació que diferents peces del software funcionen juntes. Per exemple, comunicació amb la base de dades, la xarxa o el sistem d’arxius.
- Proves end to end (E2E): validació des del punt de vista de l’usuari, tractant el software com una caixa negra.
En el món real, aquesta categorització no és estricta, hi ha proves que poden estar al mig d’aquestes capes.

| . | Unitàries | Integració | End-to-end |
|---|---|---|---|
| Objectiu principal | funcions individuals | integració de funcions | funcionalitat d’usuari |
| Quantitat | Nombroses | Una mica freqüent | Escassa |
| Velocitat | Molt alta | Mitjana | Lenta |
| Freqüència d’execució | Alta, quan es desenvolupen una funció | Regular, quan es desenvolupa una funcionalitat | Quan s’acaba una funcionalitat |
| Feedback | Entrada i sortida per a la funció | Comportament problemàtic | Funcionalitat incorrecta |
| Cost | Baix: petita, fàcil d’actualitzar, executar i entendre | Moderat | Costós |
| Coneixement de l’aplicació | Acoblat al codi | Codi, bases de dades, xarxa, arxius | Inconscient |
| Beneficis | Feedback ràpid durant el desenvolupament, evitar regressions, documentació | Ús de llibreries de tercers, comprova efectes secundaris | Funcionament correcte de l’usuari |
Tècniques
Qualitat de les proves
Aquestes són algunes pistes per a aconseguir proves que siguin mantenibles:
- Les proves són tan importants com el codi.
- Prova només una funcionalitat per prova. Que sigui curta farà que sigui més clara.
- Escriure el nostre codi amb funcions petites ens ajudarà a fer proves més granulars.
- Estructura les proves amb “arrange, act, assert” o bé “given, when, then”.
- La claredat és més important que no repetir-se.
Arrange, act i assert
L’estructura recomanada d’una prova és una seqüència de tres passos:
- Arrange: prepara les entrades i els objectius de la prova.
- Act: invoca el comportament, cridant una funció, interactuant amb un API, una pàgina web, etc.
- Assert: comprova que el resultat és l’esperat.
Test Doubles
Els test doubles són una tècnica que permet introduir una versió simplificada (o falsa) de les dades o funcions reals que permeten reduir la complexitat i facilitar les proves. Aquests objectes falsos poden classificar-se, de menys a més específic, en:
- Dummy: objectes que es passen però no s’utilitzen.
- Fake: implementacions que funcionen, però no serveixen per a producció.
- Stubs: proporcionen respostes enllaunades a certes preguntes, no responent a res que no hi hagi a la prova.
- Spies: són stubs que guarden informació de com van ser cridats.
- Mocks: estan programats amb expectatives, i per tant poden comprovar si la crida no s’espera, o falta alguna crida, llençant excepcions. Per tant, abans d’utilitzar-los cal indicar quines són les expectatives. Molts desenvolupadors utilitzen “mocks” per parlar de forma general dels dobles.
Millors pràctiques:
- Si es pot, evitar els dobles i treballar amb implementacions reals.
- De vegades no és possible, per diferents raons: volem provar certs escenaris (d’èxit o error) però no els podem generar, o bé és molt lent, o no volem treballar amb les dades reals. En general, escenaris on el codi té side effects.
- Llavors ens cal simular comportaments de certa dependència o servei extern.
- Entre els possibles dobles, es convenient seleccionar el menys específic (millor un dummy que un mock).
- Un cop seleccionat el doble, millor implementar-lo que utilitzar un framework.
Test-driven development
El TDD (desenvolupament guiat per proves) és una pràctica de desenvolupament que utilitza les proves unitàries per a escriure codi, i ho fa seguint el següent procediment:
- Afegir un nou test
- Executar tots els tests. La nova prova ha de fallar.
- Escriure el codi més senzill que permeti passar el test.
- Totes les proves han de funcionar novament.
- Fer refactoring quan calgui, utilitzant els tests per assegurar-se que la funcionalitat es preserva.
Eines
Jest i Vitest
El framework de proves JS més conegut és Jest. L’alternativa més interessant és Vitest, completament compatible amb Jest, més ràpid i que funciona amb mòduls.
Les proves JS s’emparellen amb el codi que es vol testar. Per exemple, feature.js tindria les proves a feature.test.js. Dins d’aquest arxiu s’han de poder trobar crides del tipus:
test('descripció de la prova', () => {
// codi de la prova (arrange, act, assert)
});
Alternativament, també s’utilitza la funció it (sinònim) en lloc de test.
Podem utilitzar les següents funcions per a actuar abans i després de les proves:
beforeAll(() => {
// s'executa abans de totes les proves
});
beforeEach(() => {
// s'executa abans de cada prova
});
afterEach(() => {
// s'executa després de cada prova
});
afterAll(() => {
// s'executa després de totes les proves
});
Arrange
L’arrange ha de preparar les entrades i els objectius de la prova. Això pot incloure la selecció dels nodes del DOM, la creació d’objectes, la configuració de l’estat inicial, la creació de mocks, etc.
Per a fer la selecció dels nodes, podem utilitzar document.querySelector o document.querySelectorAll. Però aquesta no és la millor forma, ja que no és el que faria un usuari (veure la Testing Library).
Act
Per al pas act, podríem utilitzar JavaScript directament, però és més recomanable utilitzar una llibreria que simuli l’usuari (veure la Testing Library).
Assert
Per al pas assert, tenim l’estructura expect(receivedValue).matcher(expectedValue).
Els matchers més habituals són els següents:
toBe: per a comprovar valors primitius.toEqual: per a comprovar objectes o arrays.not.matcher(expectedValue): per a negar qualsevol matcher.toBeNull, toBeUndefined, toBeDefined, toBeTruthy, toBeFalsy: per a comprovar truthiness.toBeGreaterThan, toBeGreaterThanOrEqual, toBeLessThan, toBeLessThanOrEqual: per a números.toMatch(/.../): per a expressions regulars.toContain: per a comprovar si un array o iterable conté un ítem.toThrow: per a comprovar si es llença una excepció. Permet comprovar el text també.
Per exemple, expect(2 + 2).toBe(4) comprova que 2+2 són 4.
La Testing Library afegeix nous matchers a jest.
Testing Library
La Testing Library és un complement a jest o vitest que afegeix noves funcionalitats per a fer proves.
Aquesta llibreria conté:
- Queries, uns mètodes per trobar elements a una pàgina. getBy, queryBy i findBy.
fireEventiuserEvent, per a simular esdeveniments que permeten interactuar amb el DOM.actiwaitFor, per a esperar rerenders o actualitzacions d’estat asíncrones.- Matchers addicionals per a jest/vitest
A més, és aplicable tan a aplicacions vanilla JS com a React o d’altres frameworks.
Queries
Les queries poden ser de tres tipus, segons els seu comportament:
- getBy: retorna un element o llença una excepció si no el troba.
- queryBy: retorna un element o null si no el troba.
- findBy: retorna una promesa que es resoldrà quan trobi l’element.
També hi ha les queries per a múltiples elements:
- getAllBy: retorna un array d’elements o llença una excepció si no en troba cap.
- queryAllBy: retorna un array d’elements o un array buit si no en troba cap.
- findAllBy: retorna una promesa que es resoldrà quan trobi els elements.
El format get és el més recomanat, ja que si no troba l’element, llença una excepció i atura la prova. Es recomana utilitzar les queries en aquest ordre:
- Les queries accessibles a tothom, amb
getByRole,getByLabelText,getByPlaceholderText,getByText,getByDisplayValue. - Les queries semàntiques, amb
getByAltText,getByTitle. - Els testid, amb
getByTestId. Implica l’ús de l’atributdata-testidals elements. És el menys aconsellable, però evita l’ús d’altres atributs, com araidoclass, associats normalment a l’estil.
Exemples de queries:
const button = getByRole('button', { name: /submit/i });
const input = getByLabelText('Username');
const input = getByPlaceholderText('Username');
const text = getByText('Hello, world!');
const input = getByDisplayValue('Hello, world!');
const img = getByAltText('A close-up of a cat');
const element = getByTitle('A description of the element');
Les queries del getByRole() permeten buscar dins d’un rol. Hi ha un nombre de rols establers per l’estàndard WAI-Aria. Alguns elements tenen un valor implícit, i també es poden definir amb l’atribut role d’un element HTML.
Les opcions més comunes d’aquesta funció són:
name: relaciona els elements pel seu nom accessible.level: s’utilitza per a les funcions d’encapçalament per especificar nivells (h1,h2, etc.).expanded: per a elements ampliables (trueofalse).checked: per a la casella de selecció i els elements de ràdio (trueofalse).pressed: per als elementsbuttonque indiquen l’estat de commutació (trueofalse).selected: per a elements comoptionque indica l’estat de selecció (trueofalse).- `: inclou elements que estan visualment ocults però encara accessibles.
Alguns examples de queries:
screen.getByRole("button", { name: "Click Me" });
screen.getByRole("heading", { level: 2 });
screen.getByRole("combobox", { expanded: true });
screen.getByRole("button", { name: "Submit" });
screen.getByRole("option", { selected: true });
El name de les opcions es pot derivar de (en ordre de preferència):
aria-labelaria-labelledby- Text intern (per botons, capçaleres, etc.)
- Atribut
alt(imatges) - Elements
labelassociats (per a form inputs, etc.) - Atribut
title(si no es troba cap altre)
fireEvent i userEvent
fireEvent és una funció que permet simular esdeveniments. userEvent és una llibreria que permet simular esdeveniments com ho faria un usuari.
Per exemple, fireEvent.click(button) simula un clic sobre un botó. userEvent.click(button) també simula un clic sobre un botó, però ho fa com ho faria un usuari.
Es recomana utilitzar userEvent sempre que sigui possible, ja que simula millor el comportament d’un usuari.
act i waitFor
act permet esperar fins que s’han produït tots els rerenders associats a les accions sobre el DOM. Moltes funcions de la testing library ja l’utilitzen, com per exemple fireEvent. Però en altres casos, com per exemple quan s’interactua amb un custom hook, cal utilitzar-lo perquè cal que l’estat s’actualitzi abans de tornar a generar un esdeveniment.
waitFor és una funció que permet esperar fins que una condició asíncrona es compleixi. Per exemple, waitFor(() => getByText('text')). Aquesta funció és útil quan es vol esperar fins que un element aparegui a la pàgina, o fins que desaparegui. També és útil per a esperar fins que un element canvii.
La sintaxi és:
await waitFor(() => {
// codi que comprova la condició
});
Per canviar el temps esperat, es pot passar un objecte amb la propietat timeout:
await waitFor(() => {
// codi que comprova la condició
}, { timeout: 1000 });
Matchers addicionals
Aquesta llibreria també afegeix matchers addicionals a jest, com ara toBeVisible, toBeDisabled, toBeEmpty, toBeEnabled, toBeInvalid, toBeRequired, toBeValid, toBeVisible, toHaveTextContent, toHaveValue, toHaveAttribute, toHaveClass, toHaveStyle, toHaveFormValues, toBeChecked, toBePartiallyChecked, toHaveFocus, toHaveDescription, toHaveDisplayValue, toHaveDisplayValue, toHaveErrorMessage, toHaveLabel, toHaveLabelText, toHaveRole, toHaveTitle, toHaveAltText, toHaveAriaLabel, toHaveAriaRole, toHaveAriaSelected.
JSDOM
Es tracta de la implementació JavaScript d’un navegador sense interfície i molt més ràpid que els convencionals. Permet implementar proves automatitzades amb poca infraestructura i senzillesa.
La seva configuració es mostra en l’apartat corresponent d’aquest document. Implementa l’objecte window.document i les seves API.
Renderització
Components
Per provar components ho podem fer amb render. Cal cridar-lo abans de fer qualsevol comprovació de l’estat. Per exemple:
render(<Clickdown initialValue={3} />);
const titleEl = screen.getByText(/value is 3/i);
expect(titleEl).toBeInTheDocument();
També podríem tornar a renderitzar el component simulant que ha canviat una prop. O bé desmuntar un component per a comprovar l’efecte:
const { rerender, unmount } = render(<Clickdown initialValue={3} />);
// ...
rerender(<Clickdown initialValue={4} />); // then, check the new value...
unmount(); // then check the effect of unmounting
Hooks
Per provar els custom hooks podem utilitzar renderHook. Per exemple, si tenim un hook del comptador:
const useCounter = (initialState: number) => {
const [counter, setCounter] = useState<number>(initialState);
return {
value: counter,
increment() {
setCounter(prev => prev + 1);
}
};
};
El podríem provar així:
const { result } = renderHook(() => useCounter(0));
act(() => result.current.increment());
expect(result.current.value).toBe(1);
Com es pot veure, s’utilitza act per assegurar-se que el canvi d’estat associat a increment s’ha consolidat. També podem utilitzar waitFor per a esperar un temps un esdeveniment asíncron.
L’objecte retornat per renderHook també conté rerender i unmount, tal com passa amb el render dels components.
Configuració
Vanilla JS
La llibreria més fàcil d’utilitzar és Vitest, compatible amb la sintaxi de Jest i amb ES modules. Necessitarem npm per a fer les proves, i això requereix crear un package.json:
$ npm init -y
$ npm install --save-dev vitest jsdom @testing-library/dom @testing-library/jest-dom
Caldria editar l’entrada script del nostre package.json:
"test": "vitest --run --reporter verbose"
Si necessitem provar el DOM, caldria afegir l’environment jsdom a la capçalera de qualsevol arxiu de proves. La sintaxi de Vitest és la de Jest:
// @jest-environment jsdom
Podem fer npm test per a executar els tests.
Per a fer proves DOM amb un script utilitzant Vitest, l’arrange podria ser:
const initialHtml = fs.readFileSync("./index.html", "utf-8");
document.body.innerHTML = initialHtml;
vi.resetModules(); // recarrega mòduls
await import('./script.js'); // aplica un script que estaria a l'HTML
També és interessant importar això si volem afegir els custom matchers de jest a les nostres proves:
import '@testing-library/jest-dom/vitest';
React
Utilitzarem vitest per a les proves React.
Caldria instal.lar vitest, @testing-library/react i @testing-library/jest-dom. A més, si volem afegir matchers addicionals, podem fer-ho important-los des de vite.config.js:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
setupFiles: './src/setup_tests.js',
},
});
Llavors, setup_tests.js podria ser:
import { afterEach } from 'vitest';
import { cleanup } from '@testing-library/react';
import '@testing-library/jest-dom/vitest';
afterEach(() => {
cleanup(); // clears the jsdom after each test
});
Podem canviar l’entrada test del package.json perquè mostri els detalls dels tests:
"test": "vitest --run --reporter verbose",
Podem fer npm test per a executar els tests.
Referències
Documentació
Aquestes són algunes estratègies per a documentar codi d’aplicacions basades en TypeScript o paquets npm:
- Formats reconeguts pels IDE, com JSDoc i TSDoc.
- Generadors de documentació per a API Rest, com el format OpenAPI i l’eina Swagger.
- Generadors de documentació a partir del codi font, com TypeDoc.
Distribució
Distribució SPA
Un cop tenim la versió de producció amb els arxius estàtics, és senzill fer la distribució.
Les aplicacions SPA poden ser distribuits utilitzant CDNs. En essència, tenim una aplicació que es connectarà al nostre backend i que pot estar a qualsevol servidor de continguts estàtics. Alternativament, podem distribuir-les al mateix servidor que el backend. Això ens estalvia la gestió dels headers CORS i poder gestionar de forma particular com se serveix l’app, però ens cal configurar el backend per a servir simultàniament l’app i el backend (API).
Una altra qüestió és què distribuim del frontend.
- No hi hauria d’haver cap dada confidencial, ja què el navegador permet veure tot el codi font de la nostra aplicació JS.
- Podem esborrar tots els comentaris del codi font amb una transformació
- Hauríem d’evitar mostrar el logging.
- Utilitzar variables al font per a donar valors que canvien en entorn de desenvolupament i de producció. Per exemple, a React, tenim els arxius .env i .env.production que permeten afegir variables. Per exemple, per a tenir una URL diferent de la nostra Rest API en cada entorn.
Distribució automàtica
El procés de distribució automàtica d’aplicacions s’implementa normalment a través d’un pipeline CI/CD que automatitza totes les etapes, des de la gestió del codi fins al desplegament en producció.
A continuació s’explica un possible flux de treball.
Commit de Codi i Control de Versions
El procés comença amb la integració del codi i el seu control de versions, assegurant la traçabilitat de cada modificació.
- Actualització del Codi: Els desenvolupadors fan commit del codi en un sistema de control de versions (com Git).
- Desencadenament del Pipeline: Cada commit (o merge) activa automàticament el pipeline mitjançant webhooks o mecanismes similars.
Integració Contínua (CI)
La fase d’integració contínua compila i prova automàticament el codi per garantir-ne la qualitat.
- Compilació del Codi: El servidor CI (com Jenkins o GitLab CI) recupera el codi i el compila o construeix l’aplicació.
- Proves Automatitzades: S’executen proves unitaris, d’integració i anàlisis de codi per garantir la qualitat.
- Generació d’Artefacte: Si tot és correcte, es crea un artefacte (binari, imatge Docker, JAR, etc.).
Emmagatzematge i Versionat d’Artefactes
Els artefactes resultants es versionen i s’emmagatzemen en repositoris centralitzats per facilitar-ne la gestió.
- Versionat: L’artefacte es versiona segons un esquema (com semàntic, basat en hash o número de build).
- Repositori d’Artefactes: L’artefacte es desa en un repositori centralitzat (per exemple, Nexus, Artifactory o un registre de contenidors com Docker Hub).
Entrega Contínua (CD) a un Entorn de Staging
L’entrega contínua desplega l’artefacte en un entorn de staging on es realitzen proves més exhaustives.
- Desplegament a Staging: El pipeline CD pren l’artefacte i el desplega en un entorn de staging.
- Proves Ampliades: Es realitzen proves d’integració, de seguretat i de rendiment que simulen l’entorn de producció.
- Passos d’Aprovació: En alguns casos, es pot requerir una aprovació manual abans de procedir al desplegament final.
Desplegament Automàtic a Producció
Un cop validat en staging, l’artefacte es distribueix automàticament en producció, amb mecanismes per revertir el desplegament si cal.
- Automatització del Desplegament: Un cop completades les proves, el pipeline desplega automàticament l’artefacte a producció.
- Infraestructura com a Codi: Eines com Terraform, Ansible o Kubernetes s’utilitzen per gestionar la infraestructura i assegurar la consistència.
- Mecanismes de Rollback: Es configuren mecanismes per revertir el desplegament en cas d’errors.
Monitorització i Retroalimentació
Finalment, la monitorització constant i la retroalimentació permeten detectar incidències i optimitzar futurs desplegaments.
- Monitorització en Temps Real: Després del desplegament, es supervisa el rendiment i es recullen logs amb eines com Prometheus, ELK o New Relic.
- Alertes i Resolució d’Errors: Els errors o anomalies generen alertes, permetent una resposta ràpida per part de l’equip de desenvolupament.
- Cicle de Millora Contínua: El feedback obtingut serveix per fer ajustaments i millores en el codi, reiniciant el cicle de desenvolupament.
IA i Ciència de dades
Referències
Introducció a la IA
- Què és la IA?
- IA Estreta vs. IA General
- Test de Turing i mesura d’intel·ligència
- Enfocaments principals de la IA
- Aprenentatge Automàtic
- Representació del coneixement
- Sistemes experts moderns
- Fonaments de Dades per a IA
- Cicle de Vida de Projectes IA
- Consideracions de Producció
- Limitacions Actuals i Failure Modes
- Ètica i IA Responsable
- Tools i Ecosistema
- Evolució Històrica i Estat Actual
- Conclusió
Què és la IA?
La Intel·ligència Artificial (IA) és la branca de la informàtica que desenvolupa sistemes capaços de realitzar tasques que tradicionalment requereixen intel·ligència humana. Des d’un punt de vista de programació, penseu en la IA com en funcions complexes que mapegen inputs a outputs, però amb la capacitat d’adaptar-se i millorar el seu comportament basant-se en dades.
A diferència dels algoritmes tradicionals on escrivim regles explícites (if-else, bucles), la IA permet que el sistema aprengui aquests patrons automàticament a partir de dades d’exemple.
Paradigma tradicional vs. IA:
- Programació tradicional: Dades + Algoritme → Resultat
- Machine Learning: Dades + Resultat → Algoritme (model entrenat)
- Inferència IA: Noves dades + Model entrenat → Nou resultat
Exemples quotidians amb perspectiva tècnica:
-
Filtratge de spam: En lloc d’escriure regex complexes per detectar spam, entrenem un model amb milers d’emails etiquetats. El model aprèn patrons (paraules freqüents, estructura, remitents) que nosaltres no hauríem pensat.
-
Recomanacions: En lloc de programar regles com “si li agrada X, recomana Y”, analitza patrons de comportament de milions d’usuaris per descobrir correlacions.
-
Reconeixement d’imatges: Impossible escriure regles per reconèixer un gat. Però podem entrenar una xarxa neuronal amb milions d’imatges etiquetades.
Penseu en qualsevol sistema IA com una pipeline amb aquestes fases:
- Preprocessing: neteja i transformació de dades (normalització, encoding)
- Feature extraction: identificar característiques rellevants
- Model training/inference: el cervell del sistema
- Postprocessing: formatatge de resultats
IA Estreta vs. IA General
Aquesta distinció és clau per entendre l’abast i limitacions dels sistemes que desenvoluparem, així com les eines i arquitectures que necessitarem.
IA Estreta (Narrow AI)
L’IA estreta és com una classe especialitzada que només té mètodes per a un domini específic. És el que programem avui. Cada sistema IA actual és essencialment una instància altament optimitzada per a una tasca concreta.
Característiques des del punt de vista de desenvolupament:
- Principi de Responsabilitat Única aplicat a la IA: cada model fa una cosa i la fa bé
- APIs específiques: inputs i outputs clarament definits
- Datasets específics: entrenada només amb dades del seu domini
- Mètriques de validació específiques: accuracy per classificació, BLEU per traducció, etc.
Exemples tècnics:
- Classificador d’imatges: Input(image) → Output(label, confidence)
- Sistema de recomanacions: Input(user_id, context) → Output(List[item_id, score])
- Traductor: Input(text, source_lang, target_lang) → Output(translated_text)
Limitacions arquitectòniques:
- No hi ha transferència de coneixement entre dominis
- Cada model requereix el seu propi pipeline de dades
- Escalabilitat vertical (millora en la mateixa tasca) però no horitzontal
IA General (AGI)
L’AGI seria com un framework universal capaç de resoldre qualsevol problema cognitiu. És l’objectiu futur. Imagineu-vos un sistema que pogués ser alhora traductor, analista d’imatges, planificador de rutes, i molt més.
Reptes tècnics actuals:
- Interferència catastròfica: és un repte durant l’aprenentatge perllongat, quan un model aprèn una nova tasca, oblida les anteriors
- Transferència entre dominis: aplicar coneixement d’un domini a un altre
- Raonament de sentit comú: comprensió implícita del món
- Meta-learning: aprendre a aprendre noves tasques ràpidament
Aproximacions actuals: Els Large Language Models (LLMs) com GPT són el més proper que tenim a AGI perquè poden realitzar múltiples tasques, però encara tenen limitacions significatives.
Test de Turing i mesura d’intel·ligència
Com a desenvolupadors, necessitem mètrics objetius per avaluar si els nostres sistemes funcionen correctament i quan poden considerar-se “intel·ligents”.
Perspectiva tècnica
El Test de Turing és essencialment un prova a cegues entre sistemes: si un jutge humà no pot distingir les respostes d’una IA de les d’un humà en conversa de text, la màquina “passa” el test.
Problemes del test:
- Només avalua imitació, no intel·ligència real
- Depèn del jutge: diferents jutges, diferents resultats
- Limitat a text: ignora altres formes d’intel·ligència
- Pot ser enganyós: un chatbot amb regles ben dissenyades pot passar el test sense ser intel·ligent
Mètriques modernes
1. Mètriques específiques:
- Classificació: Accuracy, Precision, Recall, F1-score
- Regressió: MAE, MSE, RMSE
- Generació de text: BLEU, ROUGE, perplexity
- Recomanacions: NDCG, MAP, Hit Rate
2. Proves de robustesa:
- Adversarial examples: inputs dissenyats per confondre el model
- Edge cases: comportament amb dades atípiques
- Deriva de dades (data drift): com el model es comporta quan les dades canvien amb el temps
3. Mètriques computacionals:
- Latència: temps de resposta
- Throughput: requests per segon
- Empremta de memòria: RAM necessària
- Consum d’energia: cost de l’activitat computacional
Enfocaments principals de la IA
Els dos grans enfocaments de la IA són el simbòlic, que s’enfoca en codificar coneixement explícit mitjançant regles i lògica, mentre que l’aprenentatge automàtic es basa en descobrir patrons automàticament a partir de dades.
1. Enfocament Simbòlic (Top-Down)
Filosofia de desenvolupament: L’enfocament simbòlic és com programar amb regles explícites i lògica formal. És similar a la programació tradicional però amb motors d’inferència que poden aplicar regles automàticament.
Com funciona tècnicament:
- Base de coneixement: estructura de dades que emmagatzema fets i regles
- Motor d’inferència: algoritme que aplica regles per derivar noves conclusions
- Memòria de treball: estat temporal durant el procés de raonament
Paradigmes de programació similars:
- Programació declarativa: describes el què vols, no el com
- Sistemes basats en regles: similar als sistemes experts
- Programació lògica: com Prolog
Exemple conceptual - Sistema expert mèdic:
Base de coneixement:
- REGLA: Si (febre > 38°C) AND (tos persistent) → investigar infecció respiratòria
- REGLA: Si (dolor pit) AND (dificultat respirar) → prioritat alta
- FET: Pacient_123 té febre 39°C
- FET: Pacient_123 té tos persistent
Motor d'inferència deriva: investigar infecció respiratòria per Pacient_123
Avantatges per al desenvolupament:
- Debuggable: pots seguir exactament per què el sistema ha pres una decisió
- Modificable: canviar regles sense reentrenar
- Explicable: ideal per aplicacions crítiques (medicina, finances)
- Determinista: mateixos inputs sempre produeixen mateixos outputs
Limitacions tècniques:
- Escalabilitat: difícil gestionar milers de regles
- Coll d’ampolla en l’adquisició de coneixement: cal experts per definir totes les regles
- Fragilitat: petits canvis poden trencar tot el sistema
- No aprèn automàticament: cal actualització manual
Frameworks i eines modernes:
- Drools (Java), CLIPS, Apache Jena per ontologies
- Útil per a: sistemes de conformitat, validació de regles de negoci, sistemes de diagnòstic
2. Enfocament d’Aprenentatge (Bottom-Up)
Filosofia de desenvolupament: Aquest enfocament és com programar amb dades en lloc de codi. En lloc d’escriure regles explícites, proporcionem exemples i deixem que el sistema descobreixi patrons automàticament.
Com funciona tècnicament:
- Entrenament: l’algoritme ajusta paràmetres per minimitzar error en dades d’entrenament
- Validació: avalua performance en dades no vistes
- Inferència: aplica el model entrenat a noves dades
Paradigmes de programació similars:
- Basada en dades: el comportament es defineix per les dades
- Funcional: composició de transformacions matemàtiques
- Pipeline: cadena de transformacions de dades
Tipus principals:
Supervised Learning:
- Tens datasets amb parelles (input, output correcte)
- Com entrenar una funció f(x) = y amb exemples
- Aplicacions: classificació d’imatges, detecció de spam, predicció de preus
Unsupervised Learning:
- Només tens inputs, sense outputs correctes
- Com descobrir patrons ocults en dades
- Aplicacions: clustering d’usuaris, detecció d’anomalies, reducció de dimensionalitat
Reinforcement Learning:
- El sistema aprèn mitjançant prova i error amb recompenses/penalitzacions
- Com programar un agent que explora i optimitza
- Aplicacions: jocs, cotxes autònoms, trading algorítmic
Avantatges per al desenvolupament:
- Descobriment automàtic de patrons: troba patrons que un humà no veuria
- Escalabilitat: millora amb més dades
- Adaptabilitat: pot ajustar-se a canvis en les dades
- Rendiment: excel·lent en tasques com visió i llenguatge
Limitacions tècniques:
- Black box: difícil entendre per què fa certes prediccions
- Dependència de les dades: necessita grans volums de dades de qualitat
- Computacionalment intensiu: requereix recursos significatius per entrenar
- Overfitting: pot memoritzar en lloc d’aprendre patrons generals
Frameworks moderns:
- PyTorch/TensorFlow per deep learning
- Scikit-learn per a ML tradicional
- Útil per a: reconeixement de patrons, predicció, generació de contingut
3. Enfocaments híbrids
Filosofia de desenvolupament: Combinar el millor dels dos mons: la transparència del simbòlic amb la potència de l’aprenentatge.
Arquitectures típiques:
- Preprocessing simbòlic + ML: usar regles per netejar dades abans de l’aprenentatge
- ML + postprocessing simbòlic: usar regles per validar/corregir outputs del ML
- Ensemble methods: combinar múltiples models amb regles de votació
Casos d’ús ideals:
- Sistemes crítics: on necessites explicabilitat però també performance
- Dominis amb regulacions: complir regles obligatòries + optimització
- Sistemes evolutius: començar amb regles i afegir ML progressivament
Aprenentatge Automàtic
Conceptes fonamentals
Machine Learning com a optimització: Tots els algoritmes de ML són essencialment problemes d’optimització. Tenen una funció de cost (loss function) que volen minimitzar ajustant paràmetres.
- Gradient descent: l’algoritme més common per trobar el mínim d’una funció
- Backpropagation: com les xarxes neuronals calculen gradients eficientment
- Regularització: tècniques per evitar overfitting (L1, L2, dropout)
Pipeline de desenvolupament ML:
- Recollida i neteja de dades: 80% del temps en projectes reals
- Enginyeria de característiques: crear característiques rellevants
- Selecció del model: escollir algoritme adequat
- Ajustament (tuning) d’hiperparàmetres: optimitzar configuració del model
- Avaluació: validar amb mètrics apropiats
- Desplegament: posar en producció amb monitoring
Conceptes clau per depurar models:
- Bias vs Variance tradeoff: underfitting vs overfitting
- Learning curves: visualitzar com aprèn el model
- Rellevància de les característiques: quines característiques són més rellevants per al model
- Cross-validation: estimar performance real del model
Tipus d’aprenentatge
Supervised Learning - Aprenentatge supervisat:
Input: Dataset amb parelles (x, y)
Objectiu: Aprendre funció f tal que f(x) ≈ y
Avaluació: Comparar f(x_test) amb y_test
Casos d’ús:
- Classificació: output discret (spam/ham, gat/gos)
- Regressió: output continu (preu casa, temperatura)
Unsupervised Learning - Descobriment de patrons:
Input: Dataset només amb x (sense etiquetes)
Objectiu: Descobrir estructura oculta en les dades
Avaluació: Mètriques intrínseques (silhouette, inèrcia)
Casos d’ús:
- Clustering: agrupar dades similars
- Dimensionality reduction: comprimir dades mantenint informació
- Anomaly detection: trobar outliers
Reinforcement Learning - Aprenentatge per experiència:
Agent interactua amb Environment
Cada acció genera State + Reward
Objectiu: Maximitzar recompensa acumulada
Conceptes clau:
- Exploration vs Exploitation: provar coses noves vs fer el que funciona
- Q-learning: aprendre valor de cada acció en cada estat
- Policy gradient: aprendre directament la estratègia òptima
Representació del coneixement
Com organitzem i estructurem la informació perquè els sistemes IA puguin processar-la eficientment?
De Dades a coneixement
Jerarquia de l’informació des del punt de vista de sistemes:
1. Dades (dades en brut):
- Tipus: integers, strings, floats, binaris
- Emmagatzematge: bases de dades relacionals, NoSQL, fitxers
- Exemple: “25”, “vermell”, timestamp
2. Informació (dades processades):
- Tipus: dades amb context i significat
- Emmagatzematge: estructures amb schema, JSON, XML
- Exemple: {“temperatura”: 25, “color”: “vermell”, “data”: “2024-01-15”}
3. Coneixement (informació estructurada):
- Tipus: informació amb relacions i regles
- Emmagatzematge: grafos, ontologies, bases de coneixement
- Exemple: “Si temperatura > 25°C i color = vermell, llavors alerta_calor = true”
4. Saviesa (coneixement accionable):
- Tipus: coneixement aplicat a decisions
- Emmagatzematge: sistemes experts, models entrenats
- Exemple: “Activar aire condicionat automàticament quan temperatura > 25°C”
Formats de representació
En aquesta secció veurem les formes més habituals d’encapsular informació perquè els sistemes d’IA les puguin processar.
1. Vectors de característiques (dades tabulars):
És una matriu observació × característiques (numèriques o codificades) on cada fila és un exemple i cada columna un atribut. Senzill i molt freqüent.
- Quan usar: problemes clàssics de ML amb dades estructurades (logs, surveys, sensors, CRM).
- Pros: fàcil d’entendre, moltes biblioteques madures, ràpid d’iterar.
- Contres: cal neteja i enginyeria de característiques; no captura relacions riques entre entitats.
- Eines: CSV, pandas, scikit-learn.
2. Tensors per Deep Learning:
Són arrays multidimensionals que representen batches d’imatges, senyals o embeddings per a xarxes neuronals (Deep Learning).
- Quan usar: entrenament de xarxes neuronals per imatges, àudio, o qualsevol pipeline DL.
- Pros: optimitzats per GPU/TPU; molt expressius per arquitectures modernes.
- Contres: cost computacional; menys interpretables.
- Eines: NumPy, PyTorch, TensorFlow.
3. Embeddings vectorials:
Són vectors densos que codifiquen la semàntica de paraules, frases, documents o entitats.
- Quan usar: NLP (processament de llenguatge natural), cerca semàntica, clustering, cerca per similaritat, RAG (retrieval-augmented generation).
- Pros: capturen semàntica, molt útil per trobar “símils”; escalable amb índexs de NN aproximada.
- Contres: difícil d’interpretar; depèn de models preentrenats; necessita índexs per a escala.
- Eines: word2vec/GloVe, sentence-transformers, FAISS, Milvus.
4. Representacions seqüencials i Transformers:
Són codificacions de dades ordenades (text, sèries) processades per models que capturen context i dependències. Els Transformers són l’estàndard per a llenguatge actual.
- Quan usar: NLP, traducció, classificació de text, models de llenguatge, predicció temporal amb dependències llargues.
- Pros: excel·lents per dependències llargues i context; alt rendiment en NLP.
- Contres: models grans costosos; requereixen tokenització i recursos.
- Eines: Hugging Face Transformers, tokenizers, PyTorch/TensorFlow.
5. Grafos de coneixement i ontologies:
Són nodes i arestes (triples S-P-O) amb vocabularis formals (OWL/RDF) per representar relacions i permetre inferència.
- Quan usar: integració de fonts heterogènies, necessitat d’explicabilitat i inferència simbòlica.
- Pros: traçabilitat i interoperabilitat; suporten raonament lògic.
- Contres: disseny i manteniment costós; pot ser menys trivial d’escalar.
- Eines: RDF, OWL, SPARQL, Neo4j, Apache Jena, Protégé.
Sistemes experts moderns
Els sistemes experts representen l’aplicació pràctica del raonament simbòlic en dominis específics.
Arquitectura
Components principals:
- Knowledge Base: repositori de fets i regles del domini
- Inference Engine: motor que aplica regles per derivar conclusions
- Working Memory: estat temporal durant el procés de raonament
- User Interface: interfície per interactuar amb experts i usuaris
- Explanation System: capacitat d’explicar el raonament
Patrón de funcionament:
1. User query → Working Memory
2. Inference Engine consulta Knowledge Base
3. Aplica regles rellevants
4. Deriva noves conclusions
5. Actualitza Working Memory
6. Retorna resultats + explicació
Forward vs Backward Chaining
Forward Chaining (Data-driven):
- Comença amb fets coneguts
- Aplica regles per derivar nous fets
- Continua fins que no es poden derivar més conclusions
- Ús típic: sistemes de monitoring, alertes automàtiques
Backward Chaining (Goal-driven):
- Comença amb una hipòtesi o objectiu
- Busca evidència per confirmar o rebutjar la hipòtesi
- Treballa cap enrere aplicant regles
- Ús típic: sistemes de diagnòstic, sistemes de pregunta-resposta
Casos d’ús moderns
1. Sistemes de validació de regles de negoci:
- Validar transaccions financeres
- Compliance amb regulacions
- Aprovació automàtica de sol·licituds
2. Sistemes de configuració:
- Configurar productes complexos
- Optimització de sistemes
- Personalització automàtica
3. Sistemes de suport a decisions:
- Diagnòstic assistit per ordinador
- Planificació de recursos
- Gestió de riscs
Fonaments de Dades per a IA
La qualitat i gestió de dades és fonamental en qualsevol projecte IA. A diferència de la programació tradicional, en IA les dades són tan importants com el codi.
Qualitat de les dades
Principi clau: “Garbage in, garbage out” - un model entrenat amb dades dolentes sempre produirà resultats dolents, independentment de la sofisticació de l’algoritme.
Problemes comuns de qualitat de dades:
1. Dades incompletes:
- Valors que falten: camps buits que poden trencar models
- Biaix de mostreig: dades que no representen la població real
- Forats temporals: manca de dades per períodes específics
2. Dades incorrectes:
- Soroll d’etiquetes: etiquetes incorrectes en supervised learning
- Valors extrems (outliers): valors extrems que poden distorsionar l’entrenament
- Formats inconsistents: dates, números, strings en formats diferents
3. Dades envellides:
- Deriva de concepte: quan els patrons canvien amb el temps
- Obsolescència de dades: informació que ja no és rellevant
- Canvi de distribució: canvis en la distribució de les dades
Preprocessament i Feature Engineering
Feature Engineering és l’art de transformar dades brutes en representacions que els algoritmes poden entendre millor.
Transformacions típiques:
- Normalització: escalar valors numèrics al mateix rang
- Codificació categòrica: convertir categories en números (one-hot, label encoding)
- Extracció de característiques: crear noves variables a partir de les existents
- Reducció de la dimensionalitat: reduir número de variables mantenint informació
Splits de dades critics:
- Training set (70%): per entrenar el model
- Validation set (15%): per ajustar hiperparàmetres
- Test set (15%): per avaluar performance final (NEVER toquis aquestes dades fins al final)
Regla d’or: El test set ha de simular dades completament noves que el model mai ha vist.
Data Bias i les seves implicacions
Tipus de bias que afecten sistemes IA:
1. Biaix històric: les dades reflecteixen discriminacions passades 2. Biaix de representació: alguns grups estan subrepresentats 3. Biaix de mesura: errors sistemàtics en com es recullen les dades 4. Biaix d’avaluació: mètriques que afavoreixen certs grups
Exemples reals:
- Sistema de contractació que discrimina dones perquè s’entrena amb dades històriques
- Reconeixement facial que funciona pitjor amb persones de pell fosca
- Sistemes de crèdit que penalitzen certes regions geogràfiques
Cicle de Vida de Projectes IA
Els projectes IA segueixen un cicle diferent als projectes software tradicionals, amb més experimentació i iteració.
Diferències clau respecte desenvolupament tradicional
Desenvolupament tradicional:
Requisits → Disseny → Implementació → Testing → Deploy → Manteniment
Desenvolupament IA:
Problem definition → Data collection → EDA → Feature engineering →
Model selection → Training → Evaluation → Hyperparameter tuning →
Validation → Deploy → Monitoring → Retraining
Fases del projecte IA
1. Definició del problema:
- Classificació vs. Regressió vs. Clustering: quin tipus de problema és?
- Mètriques d’èxit: com mesuraràs l’èxit?
- Línia de referència: quin és el rendiment mínim acceptable?
2. Exploratory Data Analysis (EDA):
- Entendre la distribució de dades
- Identificar correlacions i patrons
- Detectar anomalies i outliers
- Visualitzar relacions entre variables
3. Experimentació iterativa:
- Desenvolupament basat en hipòtesis: cada experiment testeja una hipòtesi
- A/B testing: comparar diferents enfocaments
- Prototipatge ràpid: fallar ràpid i aprendre
4. Validació del model:
- Validació creuada (Cross-validation): tècnica per avaluar la capacitat de generalització del model mitjançant la seva avaluació repetida en diferents subconjunts de dades
- Proves de retenció (Hold-out): avaluació final del model amb un conjunt de dades completament separat que no s’ha utilitzat durant l’entrenament
- Statistical significance: comprovar si els resultats observats del model són robustos i no deguts a l’atzar
Seguiment i versionat d’experiments
Conceptes clau per organitzar experiments:
Versionat del model:
- Cada model entrenat és una versió diferent
- Cal trackear: codi, dades, hiperparàmetres, mètriques
- Eines: MLflow, Weights & Biases, Neptune
Versionat de les dades:
- Les dades canvien amb el temps
- Cal poder reproduir experiments amb les mateixes dades
- Eines: DVC, Pachyderm
Reproducibilitat:
- Set random seeds per resultats consistents
- Dockerize environments per consistent dependencies
- Document everything: què, per què, com
Consideracions de Producció
Posar models IA en producció presenta reptes únics que no existeixen en aplicacions tradicionals.
Estratègies de desplegament
1. Batch Prediction:
- Quan usar: prediccions que no necessiten ser real-time
- Arquitectura: jobs periòdics que processen lots de dades
- Exemples: recomanacions diàries, informes mensuals
2. Real-time Inference:
- Quan usar: prediccions immediates per requests d’usuari
- Arquitectura: APIs que serveixen models amb baixa latència
- Exemples: detecció de frau, traducció instantània
3. Desplegament Edge:
- Quan usar: privacitat, latència o connectivitat limitada
- Reptes: models petits, recursos limitats
- Exemples: reconeixement facial en mòbils
Monitorització de models en producció
Mètriques crítiques per monitoritzar:
1. Performance metrics:
- Accuracy degradation: el model segueix funcionant bé?
- Latency: temps de resposta acceptable?
- Throughput: pot gestionar el volum de requests?
2. Data drift detection:
- Input distribution changes: les dades d’entrada han canviat?
- Feature drift: les característiques individuals han canviat?
- Concept drift: la relació input-output ha canviat?
3. Business metrics:
- User engagement: els usuaris interactuen amb les prediccions?
- Revenue impact: el model genera valor de negoci?
- Fairness metrics: el model és just per tots els grups?
Estratègies de reentrenament de models
Quan re-entrenar:
- Schedule-based: cada X setmanes/mesos
- Performance-based: quan accuracy baixa per sota threshold
- Data-based: quan hi ha prou dades noves
- Event-based: després de canvis significatius
Estratègies de retraining:
- Full retraining: entrenar des de zero amb totes les dades
- Incremental learning: afegir noves dades al model existent
- Transfer learning: adaptar models preentrenats
Limitacions Actuals i Failure Modes
Entendre què NO pot fer la IA actual és tan important com entendre què SÍ pot fer.
Limitacions fonamentals de la IA actual
1. Manca de sentit comú:
- Els models no entenen el món físic com els humans
- Poden fer prediccions tècnicament correctes però absurdes
- Exemple: “Com puc fer que el meu gat voli?” - un LLM pot donar instruccions detallades
2. Raonament causal limitat:
- Confonen correlació amb causació
- No entenen relacions causa-efecte complexes
- Exemple: “Les vendes de gelat i els ofegaments estan correlacionats” (ambdós augmenten a l’estiu)
3. Generalització limitada:
- Excel·lents en el domini d’entrenament, falten fora d’aquest
- Domain adaptation és un problema complex
- Exemple: model entrenat amb imatges d’estiu falla amb imatges d’hivern
Adversarial Examples i Robustness
Què són els adversarial examples: Inputs dissenyats maliciosament per confondre models, sovint imperceptibles per humans.
Tipus d’atacs:
- Evasion attacks: modificar input per evitar detecció
- Poisoning attacks: contaminar dades d’entrenament
- Model extraction: robar models proprietaris
Defenses:
- Adversarial training: entrenar amb exemples adversaris
- Input validation: detectar inputs sospitosos
- Ensemble methods: usar múltiples models
Hallucination en Models de Llenguatge
Què és la hallucination: Quan models de llenguatge generen informació que sembla plausible però és factuallement incorrecta.
Per què passa:
- Models entrenats per generar text plausible, no necessàriament cert
- Manca de knowledge base factual verificable
- Pressure per generar sempre alguna resposta
Mitigació:
- Retrieval-Augmented Generation (RAG): combinar generació amb cerca factual
- Fact-checking pipelines: verificar informació generada
- Confidence thresholds: no generar respostes quan incert
La importància de Human Oversight
Principi clau: La IA ha de complementar, no reemplaçar, el judici humà en decisions crítiques.
Nivells de automatització:
- Human in the loop: humà pren totes les decisions finals
- Human on the loop: humà supervisa i pot intervenir
- Human out of the loop: automatització completa (només per tasques no crítiques)
Ètica i IA Responsable
Desenvolupar IA responsable no és opcional - és una responsabilitat professional.
Algorithmic Bias i Fairness
Definicions de fairness:
1. Individual fairness: Individus similars han de rebre tracte similar
2. Group fairness: Diferents grups demogràfics han de tenir outcomes similars
3. Counterfactual fairness: Les decisions haurien de ser les mateixes en un món contrafactual
Tècniques de mitigació:
- Pre-processing: eliminar bias de les dades d’entrenament
- In-processing: modificar algoritmes per ser més justos
- Post-processing: ajustar outputs per garantir fairness
Privacy i Protecció de Dades
Principals preocupacions:
1. Data minimization:
- Recollir només les dades estrictament necessàries
- Implementar retention policies per esborrar dades antigues
- Anonymization i pseudonymization quan sigui possible
2. Differential Privacy:
- Afegir soroll calculat per protegir individuals
- Trade-off entre privacy i utility
- Implementació en training i inference
3. Federated Learning:
- Entrenar models sense centralitzar dades
- Cada participant manté les seves dades localment
- Útil per sectors altament regulats (sanitat, finances)
Transparència i Explicabilitat
Nivells d’explicabilitat:
1. Global explanations: Com funciona el model en general?
2. Local explanations: Per què el model ha fet aquesta predicció específica?
3. Counterfactual explanations: Què hauria de canviar perquè la predicció fos diferent?
Tècniques:
- LIME: explica prediccions individuals
- SHAP: valors de contribució de cada feature
- Attention visualization: per models de deep learning
Impacte Ambiental
Carbon footprint dels models IA:
- Training de models grans consumeix energia equivalent a vols intercontinentals
- Inference a escala masiva també té impacte significatiu
Estratègies de mitigació:
- Model efficiency: models més petits amb performance similar
- Green computing: usar energia renovable per data centers
- Carbon offsetting: compensar emissions amb projectes ambientals
Tools i Ecosistema
Panoràmica de les eines essencials que necessitareu per desenvolupar projectes IA.
Frameworks de Machine Learning
Python Ecosystem (dominant):
- Scikit-learn: ML tradicional, perfecte per començar
- TensorFlow/Keras: deep learning, ecosistema complet
- PyTorch: deep learning, més flexible per research
- Transformers (Hugging Face): NLP state-of-the-art
Altres llenguatges:
- R: estadístiques i anàlisi de dades
- Julia: computing científic d’alt rendiment
- JavaScript: ML en el browser (TensorFlow.js)
Plataformes Cloud per a IA
Proveïdors principals del núvol:
- AWS: SageMaker, comprehensive ML platform
- Google Cloud: Vertex AI, forte en research i models pretrained
- Azure: Machine Learning Studio, integració amb ecosistema Microsoft
Managed vs. Self-managed:
- Managed: més cars però menys sobrecàrrega
- Self-managed: més control però més complexitat operacional
APIs vs. construir des de zero
Quan usar APIs:
- Tasques comunes: traducció, sentiment analysis, reconeixement d’imatges
- Prototipatge ràpid: validar la idea abans d’invertir en desenvolupament
- No tens experiència: àrees fora de la teva especialització
Quan construir des de zero:
- Requeriments específics: cap API pública cobreix el teu cas d’ús
- Data sensitivity: no pots enviar dades a tercers
- Cost a escala: APIs poden ser cares per grans volums
APIs populars:
- OpenAI GPT: generació de text i codi
- Google Vision: anàlisi d’imatges
- AWS Comprehend: sentiment analysis
- Azure Cognitive Services: múltiples tasques de IA
Evolució Històrica i Estat Actual
Perspectiva tecnològica de l’evolució
Primers sistemes (1950s-1980s):
- Limitacions: processament seqüencial, poca memòria, algoritmes simples
- Enfocament: purament simbòlic
- Resultats: sistemes molt especialitzats però fràgils
Revival del ML (1990s-2000s):
- Estímul: més dades disponibles, millor poder computacional
- Avenços: Support Vector Machines, Random Forests
- Limitacions: encara calia feature engineering manual
Era del Big Data i Deep Learning (2010s):
- Estímul: GPU computing, datasets massius, algoritmes millorats
- Avenços: ImageNet (2012) amb convolutional neural networks
- Canvi paradigmàtic: de feature engineering manual a feature learning automàtic
Era dels Transformers i LLMs (2017-actualitat):
- Estímul: architecture Transformer, transfer learning, scaling laws
- Avenços: models com GPT, BERT que entenen llenguatge natural
- Paradigma: pre-training en dades massives + fine-tuning per tasques específiques
Tendències actuals per a desenvolupadors (2024-2025)
1. Models Fonamentals (Foundation Models):
- Concepte: models enormes entrenats en dades diversas que es poden adaptar a múltiples tasques
- Implicacions per desenvolupament: menys entrenament from scratch, més fine-tuning i prompting
- Exemples: GPT-4, Claude, Gemini
2. Multimodalitat:
- Concepte: models que processen text + imatge + àudio simultàniament
- Implicacions: aplicacions més riques, interfaces més naturals
- Reptes: integrar diferents tipus de dades eficientment
3. Agents Autònoms:
- Concepte: sistemes que poden planificar i executar tasques complexes
- Components: LLM + eines externes + memòria + planificació
- Aplicacions: automatització de tasques, assistents de programació
4. Edge AI:
- Concepte: executar models IA directament en dispositius locals
- Motivació: latència, privacitat, costs de cloud
- Reptes tècnics: optimització de models, quantització, pruning
Reptes actuals
1. Reptes tècnics:
- Explicabilitat: com fer models més interpretables
- Robustesa: models que funcionin bé amb dades noves
- Eficiència: reduir cost computacional i energètic
- Seguretat: protegir models contra atacs adversaris
2. Reptes d’enginyeria:
- MLOps: automatitzar pipeline de ML en producció
- Governança dels models: gestionar versions, qualitat, compliance
- Qualitat de les dades: assegurar dades netes i representatives
- Monitorització: detectar degradació de performance en producció
3. Reptes ètics i socials:
- Mitigació del biaix: evitar discriminació en models
- Privacitat: protegir dades personals en entrenament
- Equitat: assegurar equitat en decisions automàtiques
- Transparència: explicar decisions d’IA a stakeholders
Conclusió
La Intel·ligència Artificial ha evolucionat d’un camp purament acadèmic a una disciplina d’enginyeria pràctica. Com a programadors, tenim l’oportunitat i la responsabilitat de desenvolupar sistemes que augmentin les capacitats humanes de manera ètica i eficient.
Punts clau per recordar:
- Escollir l’enfocament adequat segons el problema: simbòlic per explicabilitat, ML per pattern recognition
- Entendre les limitacions de cada paradigma i planificar en conseqüència
- Mantenir-se actualitzat amb l’evolució ràpida del camp
- Considerar implicacions ètiques en el desenvolupament d’aplicacions IA
L’futur de la programació està íntimament lligat amb la IA, i dominar aquests conceptes és essencial per al desenvolupament de software modern.
Aprenentatge Automàtic
Fonaments teòrics i pràctics dels principals algoritmes de machine learning, des de la regressió fins als sistemes de recomanació.
- Aprenentatge supervisat: Regressió lineal, logística i els fonaments d’aprendre a partir de dades etiquetades.
- Mètriques d’avaluació: R², precisió, recall, F1 i altres mètriques per mesurar el rendiment dels models.
- Xarxes neuronals: Del perceptró al deep learning, arquitectura i entrenament de xarxes multicapa.
- Guia d’aplicació i diagnòstic: Tècniques de diagnòstic matemàtic per identificar i resoldre problemes en models.
- Arbres de decisió: Classificació basada en particions recursives de l’espai de features.
- K-veïns més propers: Classificació per similitud amb els exemples d’entrenament més propers.
- Aprenentatge no supervisat: Clustering, detecció d’anomalies i reducció de dimensionalitat sense etiquetes.
- Recomanadors: Sistemes per predir preferències d’usuaris amb filtrat col·laboratiu.
- Metodologia pràctica: Guia de decisió per triar algoritmes i estructurar projectes de ML.
Aprenentatge supervisat
- Introducció
- Terminologia fonamental
- Funció de cost
- Gradient Descent (Descens del Gradient)
- Descens del gradient alternatius
- Regressió Lineal Múltiple
- Escalat i normalització de característiques
- Convergència del Descens del Gradient
- Enginyeria de Característiques
- Regressió Polinòmica
- Regressió lineal amb Scikit-learn
- Classificació binària
- Regressió logística amb Scikit-learn
- Sobreajustament i Subajustament
Introducció
L’aprenentatge automàtic (Machine Learning) és una branca de la intel·ligència artificial que permet als algoritmes aprendre i millorar automàticament a partir de l’experiència, sense ser explícitament programats. Existeixen dos tipus principals d’aprenentatge: supervisat i no supervisat.
L’aprenentatge supervisat és un mètode on l’algoritme aprèn a partir de dades etiquetades, és a dir, se li proporcionen les respostes correctes durant l’entrenament.
Per contra, l’aprenentatge no supervisat treballa amb dades sense etiquetes, buscant patrons o estructures ocultes en les dades.
Característiques principals de l’aprenentatge supervisat:
- L’algoritme troba una línia o corba que representa la sortida donada l’entrada
- Utilitza dades d’entrenament amb exemples d’entrada i sortida coneguts
- L’objectiu és generalitzar per fer prediccions sobre dades noves
Tipus d’aprenentatge supervisat
Regressió
- Objectiu: Predir un número d’un conjunt infinit de possibles sortides
- Exemple: Predir el preu d’una casa basant-se en la seva mida
- Sortida: Valor numèric continu
Classificació
- Objectiu: Predir una categoria d’un conjunt finit de possibles valors (2 o més)
- Exemple: Determinar si un correu és spam o no
- Sortida: Classe o categoria discreta
- Funcionament: L’algoritme troba fronteres per separar les categories
Quan usar regressió vs classificació?
- Usa regressió quan la sortida és un valor continu (preu, temperatura, pes…)
- Usa classificació quan la sortida és una categoria discreta (spam/no spam, maligne/benigne…)
Terminologia fonamental
Variables i dades
- Dataset d’entrenament: Conjunt de dades utilitzat per entrenar el model
- x: Variable d’entrada o característica (feature)
- y: Variable de sortida o objectiu (target)
- (xᵢ, yᵢ): i-èssim exemple d’entrenament (una fila del dataset)
- m: Nombre d’exemples d’entrenament
Model i predicció
- f(x) = ŷ: Funció que representa el model i fa prediccions
- ŷ: Estimació o predicció del model (y amb accent circumflex)
- y: Valor real o objectiu
Model lineal
- f(x) = wx + b: Regressió lineal amb una variable
- w: Paràmetre de pes (weight) del model
- b: Paràmetre de biaix (bias) del model
- Els paràmetres w i b s’ajusten durant l’entrenament per millorar el model
Al següent gràfic es representa, a la coordenada x, una característica. I a la y, l’etiqueta. Com es tracta d’un model lineal, es representa com una línia.
Funció de cost
La funció de cost és una eina matemàtica fonamental que avalua globalment com de bé o malament s’ajusta el model al conjunt de dades. Es defineix habitualment com la mitjana de les funcions de pèrdua individuals, que mesuren l’error per a cada mostra. D’aquesta manera, la funció de cost assigna un valor numèric que representa l’“error total” del model: com més gran sigui aquest valor, pitjor és el rendiment, i com més petit sigui, millors són les seves prediccions.
Funció de cost d’error quadràtic
La funció de cost més utilitzada per a regressió lineal és l’error quadràtic:
J(w,b)=12mm∑i=1(f(xi)−yi)2
En paraules senzilles: Calculem l’error de cada predicció (diferència entre predicció i valor real), l’elevem al quadrat, i en fem la mitjana.
On:
- J(w, b): Funció de cost
- m: Nombre d’exemples d’entrenament
- f(xᵢ): Predicció per a l’exemple i
- yᵢ: Valor real per a l’exemple i
Per què aquesta fórmula?
- Divisió per m: Perquè l’error no depengui del nombre d’exemples d’entrenament
- Divisió per 2: Simplifica els càlculs futurs (especialment les derivades)
- Elevat al quadrat: Penalitza més els errors grans, evita que errors positius i negatius es cancel·lin, i fa la funció diferenciable a tot arreu
Objectiu d’Optimització: minimitzar J(w, b) per trobar els millors paràmetres del model.
Visualització:
- Si canviem només w: Obtenim una corba en forma de U
- Si canviem w i b: Obtenim una superfície 3D en forma de U
- El punt més baix representa els millors paràmetres
Aquesta és la representació quan tenim dues característiques:
Gradient Descent (Descens del Gradient)
És l’algoritme que troba els valors de w i b que minimitzen la funció de cost J(w, b).
Funcionament:
- Inicialització: Comença amb w = 0 i b = 0
- Iteració: Canvia w i b per reduir J(w, b)
- Direcció: Fa passos en la direcció que més redueix la funció de cost
- Convergència: S’atura quan arriba a un mínim local
Derivades i derivades parcials
Abans d’entendre l’algorisme matemàtic, necessitem comprendre què és una derivada.
Una derivada ens diu com de ràpid canvia una cosa.
Imagina que estàs en una muntanya russa. En cada moment:
- L’altura f(x) et diu on estàs. És la quantitat que canvia.
- La derivada de l’altura f’(x) et diu com de ràpid estàs pujant o baixant, és a dir, la pendent:
- Si la derivada és positiva, estàs pujant.
- Si és negativa, estàs baixant.
- Si la derivada és 0, estàs en un punt pla (ni puges ni baixes).
La gràfica mostra:
- La corba és f(x) = x^2
- La línea de punts és la tangent al punt seleccionat del slider
- La pendent de la línia és la derivada f’(x_0) = 2x_0
I què és una derivada parcial respecte d’una variable? Vol dir que la funció depèn de més d’una variable. Per exemple:
f(x, y) = x^2 + y^2
Aquí, la funció depèn de x i y.
La derivada parcial respecte de x és com canvia f si només canvies x, deixant y fix.
La derivada parcial respecte de y és com canvia f si només canvies y, deixant x fix.
🔍 És com mirar la pendent en una sola direcció, mentre les altres es mantenen iguals.
Algorisme del Gradient Descent
Ara que entenem què són les derivades, podem veure l’algorisme matemàtic:
w \mathrel{:=} w - \alpha \times \frac{\partial J}{\partial w} \\ b \mathrel{:=} b - \alpha \times \frac{\partial J}{\partial b}
Què significa això?
- Calculem la derivada parcial (la pendent) de la funció de cost respecte a cada paràmetre
- Aquesta pendent ens diu en quina direcció augmenta l’error
- Restem un petit pas en aquesta direcció per reduir l’error
- α (alfa) controla la mida del pas que fem
En paraules senzilles: “Mira en quina direcció puja la muntanya (derivada) i fes un pas cap a l’altra banda (resta) per baixar.”
Consideracions Importants
Actualització Simultània
És crucial actualitzar w i b simultàniament en cada iteració per garantir el funcionament correcte de l’algoritme.
Taxa d’Aprenentatge (α)
- α massa petita: L’algoritme funcionarà però serà lent
- α massa gran: L’algoritme pot no convergir i “saltar” el mínim
- α adequada: Convergència eficient cap al mínim
Convergència Natural
A mesura que ens apropem al mínim, els passos es fan més petits automàticament perquè la derivada (pendent) disminueix.
Mínims Locals vs Globals
- Mínim local: El punt més baix d’una “vall” específica
- Mínim global: El punt més baix de tota la funció
- El punt d’inici pot determinar a quin mínim local arribem
Propietat Especial de la Regressió Lineal
La funció de cost d’error quadràtic amb regressió lineal té una propietat única: només té un mínim, que és el global. Això és perquè la funció de cost té forma de bol (funció convexa).
Per què importa la convexitat? Una funció convexa té la propietat que qualsevol mínim local és també un mínim global. Això garanteix que el descens del gradient sempre trobarà la millor solució possible, independentment del punt d’inici.
Descens del gradient alternatius
Batch (per lots complets) és la versió del gradient descent que utilitza tots els exemples d’entrenament en cada pas de l’algoritme. Aquesta és la versió que utilitzarem per a regressió lineal, tot i que existeixen alternatives que utilitzen subconjunts de dades per millorar el rendiment en datasets molt grans:
- Gradient Descent per mini-lots (Mini-Batch): actualitza els pesos usant un petit subconjunt de mostres, equilibrant velocitat i estabilitat.
- Gradient Descent estocàstic (SGD): actualitza els pesos usant una sola mostra a la vegada, introduint actualitzacions sorolloses però freqüents.
Regressió Lineal Múltiple
En regressió lineal múltiple utilitzem múltiples característiques (variables) per ajudar a predir.
Notació:
-
Les característiques es numeren: x_1, x_2, x_3, \ldots Exemple: x_1 = mida, x_2 = habitacions, x_3 = edat
-
Per referir-nos a un exemple específic, posem un número entre parèntesis: \vec{x}^{(2)} = totes les característiques de la casa número 2 x_3^{(2)} = només l’edat de la casa número 2
Model de la Regressió Multiple
El nou model es defineix com:
f_{\vec{w},b}(\vec{x}) = \vec{w} \cdot \vec{x} + b = w_1 x_1 + w_2 x_2 + \dots + w_n x_n + b
On:
- \vec{w} = [w_1, w_2, w_3, …, w_n] són els paràmetres del model
- \vec{x} = [x_1, x_2, x_3, …, x_n] són les característiques
I \cdot representa el producte escalar (dot product), una operació entre dos vectors de la mateixa mida que retorna un escalar. L’operació es pot vectoritzar, que vol dir que es pot escriure sense bucles i executar-se amb una llibreria numèrica optimitzada per a matrius.
Codi Python
Sense vectorització:
import numpy as np
w = np.array([w1, w2, w3, ...])
b = 4
x = np.array([x1, x2, x3, ...])
f = 0
n = len(w)
for j in range(n):
f += w[j] * x[j]
f += b
Amb vectorització:
import numpy as np
w = np.array([w1, w2, w3, ...])
b = 4
x = np.array([x1, x2, x3, ...])
f = np.dot(w, x) + b
Gradient Descent per Múltiples Variables
L’algorisme de descens del gradient s’actualitza per a cada paràmetre w_j amb el següent pas:
w_j := w_j - \alpha \frac{\partial J(\vec{w}, b)}{\partial w_j} \quad \text{for } j = 1 \dots n\\ b := b - \alpha \frac{\partial J(\vec{w}, b)}{\partial b}
Escalat i normalització de característiques
Imagina que vols predir el preu d’una casa amb dues característiques:
- Mida: 50-300 m² (rang molt gran)
- Habitacions: 1-5 (rang petit)
El model tindrà dificultats perquè les escales són molt diferents.
Per què és un problema? Quan les característiques tenen rangs molt diferents, el descens del gradient es mou de forma ineficient, fent “zigzag” en lloc de baixar directament cap al mínim. Això fa que l’entrenament sigui molt més lent.
La solució: Transformar totes les característiques perquè tinguin escales similars. Això fa que el descens del gradient sigui molt més directe i ràpid.
Terminologia: escalat vs normalització
En aprenentatge automàtic, aquests termes s’utilitzen sovint de manera intercanviable, però tècnicament tenen significats diferents:
- Escalat (scaling): Transformació que mapeja cada característica a un rang específic predefinit (per exemple, [0, 1] o [-1, 1]). El mètode més comú és l’escalat Min-Max, que preserva la forma de la distribució original.
- Normalització (normalization): En sentit estricte, qualsevol transformació aplicada per aconseguir una propietat estadística desitjada (com ara centrar les dades, ajustar el rang, o obtenir una distribució amb norma unitària). En la pràctica, sovint s’utilitza de manera més àmplia com a terme genèric per referir-se a qualsevol tipus d’escalat o transformació de característiques.
- Estandardització / z-score (standardization): Cas particular de normalització que transforma les dades per tenir mitjana zero i desviació estàndard igual a u (μ = 0, σ = 1). Aquesta transformació, implementada per exemple en
StandardScaler, s’aplica de manera independent a cada característica i és especialment útil quan les dades segueixen aproximadament una distribució normal.
Mètodes de transformació
1. Escalat mínim-màxim (Min-Max Scaling)
Transforma les dades a un rang [0, 1]:
x_i^{\text{esc}} = \frac{x_i - \min(x)}{\max(x) - \min(x)}
Quan usar-lo: Quan necessites que totes les característiques estiguin en el mateix rang [0, 1]. Útil per algoritmes sensibles a la magnitud dels valors.
Desavantatge: Sensible a outliers (valors extrems).
2. Normalització per la mitjana (Mean Normalization)
Centra les dades al voltant de 0 i les escala pel rang:
x_i^{\text{norm}} = \frac{x_i - \mu_i}{\max(x) - \min(x)}
On \mu_i és la mitjana de la característica.
Quan usar-lo: Quan vols centrar les dades però mantenir un rang específic aproximadament entre -0.5 i 0.5.
3. Estandardització z-score (Standardization)
Transforma les dades per tenir mitjana 0 i desviació estàndard 1:
x_i^{\text{std}} = \frac{x_i - \mu_i}{\sigma_i}
On \mu_i és la mitjana i \sigma_i és la desviació estàndard.
Quan usar-lo: El mètode més comú en ML. Menys sensible a outliers que Min-Max. És el que utilitza StandardScaler de scikit-learn.
Avantatge: No té límits definits (pot donar valors fora de [-1, 1] si hi ha outliers), però la majoria de valors queden típicament entre -3 i 3.
Guia pràctica
Els valors resultants solen quedar en rangs raonables. Si les teves característiques ja estan entre -0.3 i 0.3, o entre -3 i 3, potser no cal aplicar cap transformació.
Regla general: Usa estandardització Z-score per defecte, excepte quan necessitis específicament un rang [0, 1] (llavors usa Min-Max).
Convergència del Descens del Gradient
Per visualitzar la convergència, es representa la funció de cost J(w, b) al llarg de les iteracions. Aquesta és la corba d’aprenentatge, que sovint té forma de llei de potències.
- Si J augmenta, potser el pas \alpha és massa gran.
- Després de moltes iteracions, J pot deixar de disminuir — això és la convergència.
Test automàtic de convergència: un criteri simple és definir un llindar de tolerància \varepsilon = 0.001 (epsilon, un valor molt petit). Si el canvi de J entre iteracions és inferior a \varepsilon , considerem que ha convergit. Això és només un dels molts criteris possibles per determinar la convergència.
Elecció del Ritme d’Aprenentatge
- Massa gran: J augmenta o oscil·la per excés de pas.
- Massa petit: convergència molt lenta.
Una seqüència típica de valors per provar seria: \alpha \in { 0.001, 0.003, 0.01, 0.03, 0.1, 0.3 }
Per avaluar quin valor d’ \alpha funciona millor, podem utilitzar un conjunt de validació o tècniques com K-Fold Cross-Validation. Si cap funciona, cal revisar la implementació.
Enginyeria de Característiques
Fins ara hem utilitzat les característiques tal com ens les donaven. Però sovint podem millorar significativament el model creant noves característiques més informatives.
L’enginyeria de característiques consisteix a crear noves característiques a partir de les existents utilitzant el nostre coneixement del problema.
Exemple pràctic: Si volem predir el preu d’una casa i tenim:
façana= 10 metresfons= 20 metres
Podem crear una nova característica més útil:
superfície= façana × fons = 200 m²
Sovint, aquesta nova característica és més informativa que les originals per si soles. És com donar-li al model la informació ja “processada” de forma més útil.
Regressió Polinòmica
Què passa si les dades no segueixen una línia recta sinó una corba?
La regressió polinòmica ens permet ajustar corbes als nostres dades afegint termes com x^2 , x^3 , etc.
Exemple: y = w_1 x + w_2 x^2 + b
En lloc de només x (línia recta), també usem x^2 (això crea una corba parabòlica).
Important: Quan afegim termes com x^2 o x^3 , els valors poden créixer molt (per exemple, 100^2 = 10000 ). Per això, l’escalat de característiques és essencial en regressió polinòmica.
També es poden fer altres transformacions com \sqrt{x} segons el problema.
Regressió lineal amb Scikit-learn
Implementarem amb scikit-learn un model per predir el preu d’una casa.
Assumim que el conjunt de dades es troba en un fitxer anomenat ‘houses.txt’ amb la primera fila com a capçalera i les característiques estan separades per comes. L’última columna és la variable objectiu (preus de les cases). El conjunt de dades s’ha d’estructurar de la següent manera: size(sqft),bedrooms,floors,age,price.
import numpy as np
from sklearn.linear_model import SGDRegressor
from sklearn.preprocessing import StandardScaler
# Carregueu el conjunt de dades
data = np.loadtxt("./data/houses.txt", delimiter=",", skiprows=1)
X_train = data[:, :4]
y_train = data[:, 4]
X_features = ["size(sqft)", "bedrooms", "floors", "age"]
# Normalitzar les característiques
# Ús de StandardScaler per normalitzar les característiques
scaler = StandardScaler()
X_norm = scaler.fit_transform(X_train)
print(f"Peak to Peak range by column in Raw X:{np.ptp(X_train,axis=0)}")
print(f"Peak to Peak range by column in Normalized X:{np.ptp(X_norm,axis=0)}")
# Entrena un model de regressió lineal utilitzant SGDRegressor
sgdr = SGDRegressor(max_iter=1000)
sgdr.fit(X_norm, y_train)
print(sgdr)
print(
f"number of iterations completed: {sgdr.n_iter_}, number of weight updates: {sgdr.t_}"
)
# Mostrar els paràmetres del model
b_norm = sgdr.intercept_
w_norm = sgdr.coef_
print(f"model parameters: w: {w_norm}, b:{b_norm}")
# Feu prediccions sobre el conjunt d'entrenament
y_pred_sgd = sgdr.predict(X_norm)
y_pred = np.dot(X_norm, w_norm) + b_norm
print(
f"prediction using np.dot() and sgdr.predict match: {(y_pred == y_pred_sgd).all()}"
)
print(f"Prediction on training set:\n{y_pred[:4]}")
print(f"Target values \n{y_train[:4]}")
El gràfic amb 4 subplots mostra cadascuna de les característiques respecte del preu real (punts blaus) i el preu predit (punts taronges). La línia vermella mostra la tendència real de cada estadística.

El que busquem és:
- Bon rendiment del model: els punts taronges haurien d’estar a prop dels punts blaus a cada gràfic.
- Relacions fortes: els valors de R² més alts signifiquen que la característica prediu fortament el preu: com de compactes s’agrupen les dades al voltant de la línia.
- Supòsits lineals: la línia de tendència vermella mostra si una relació lineal té sentit per a cada característica.
Per a les característiques discretes (dormitoris i pisos), es creen “columnes” verticals de punts perquè les cases només poden tenir nombres enters, cosa que fa que el format del diagrama de dispersió sigui menys intuïtiu.
Classificació binària
Imagina que vols separar dos tipus d’objectes diferents. La classificació binària funciona combinant les característiques dels teus exemples (les variables d’entrada) de manera similar a com ho fa la regressió lineal. El truc està en traçar una línia de separació (en 2D) o un pla de separació (en 3D) que divideix l’espai en dues regions: una per a cada classe. Aquest model s’anomena regressió logística.
Els 3 passos de la regressió logística
Per entendre com funciona realment la regressió logística, anem a desglossar el procés en 3 passos clars:
Pas 1: Calcular una puntuació (la combinació lineal):
Primer, calculem una puntuació per cada exemple combinant les seves característiques:
z = w \cdot x + b
On:
- w són els pesos que ajustem durant l’entrenament
- x són les característiques d’entrada de l’exemple
- b és el terme independent (bias)
- z és la puntuació resultant
Exemple concret: Si volem predir si algú comprarà un producte segons la seva edat (30 anys) i ingressos (60.000€):
z = -5 + 0.1×30 + 0.00003×60.000 = -0.2
Aquesta puntuació z pot ser qualsevol número (de -∞ a +∞). Però què significa? Encara no ho sabem… necessitem interpretar-la!
Pas 2: Interpretar la puntuació (el log odds):
Aquí ve la idea clau: aquesta puntuació z que acabem de calcular ja és el logaritme de les oportunitats (log odds) de pertànyer a una classe o l’altra!
Què vol dir això?
- Si z = 0 → 50% de probabilitat per cada classe
- Si z > 0 → més probable que sigui de la classe 1
- Si z < 0 → més probable que sigui de la classe 0
- Com més gran el valor absolut de z, més segurs estem de la predicció
Pensa-ho com una línia numèrica:
Segur classe 0 Dubte Segur classe 1
-∞ <----------- 0 -----------> +∞
En el nostre exemple, z = -0.2 està una mica a l’esquerra del zero, així que l’exemple probablement pertany a la classe 0, però no estem molt segurs.
Pas 3: Convertir a probabilitat (la funció sigmoide):
Finalment, necessitem convertir aquesta puntuació z en una probabilitat que tothom pugui entendre (un número entre 0 i 1). Per això fem servir la funció sigmoide:
g(z) = \frac{1}{1 + e^{-z}} \quad \text{on} \quad 0 < g(z) < 1
Per què és útil aquesta funció? Perquè agafa qualsevol número (sigui molt gran, molt petit, positiu o negatiu) i el “comprimeix” suaument a un valor entre 0 i 1.
La sigmoide té forma d’S i funciona així:
z = -10 → g(z) ≈ 0.00005 (0%) ← Gairebé segur classe 0
z = -2 → g(z) ≈ 0.12 (12%)
z = 0 → g(z) = 0.5 (50%) ← Punt central, dubte total
z = 2 → g(z) ≈ 0.88 (88%)
z = 10 → g(z) ≈ 0.99995 (100%) ← Gairebé segur classe 1
En el nostre exemple amb z = -0.2:
g(-0.2) ≈ 0.45
Això vol dir que hi ha un 45% de probabilitat que l’exemple pertanyi a la classe 1, i per tant un 55% que sigui de la classe 0.
El model complet
Posant-ho tot junt, el nostre model de classificació binària és:
f_{w,b}(x) = \frac{1}{1 + e^{-(w \cdot x + b)}} = P(y = 1 \mid x; w, b)
O dit d’una altra manera, més intuïtiva:
Característiques → [Puntuació z] → [Sigmoide] → [Probabilitat]
x → w·x + b → g(z) → 0 a 1
Aquesta expressió ens dona directament la probabilitat que l’entrada x pertanyi a la classe 1, donats els paràmetres w i b que hem après durant l’entrenament.
Resum Visual
- Puntuació negativa → probabilitat propera a 0 → quasi segur que és de la classe 0
- Puntuació positiva → probabilitat propera a 1 → quasi segur que és de la classe 1
- Puntuació propera a zero → probabilitat propera a 0.5 → incertesa total, podria ser de qualsevol classe
Conceptes Clau
Log Odds (Logit): El logaritme de les oportunitats. És el valor z que obtenim de la combinació lineal. Va de -∞ a +∞ i és simètric al voltant del zero.
Sigmoide: La funció matemàtica que converteix qualsevol número real en una probabilitat (valor entre 0 i 1). Té forma d’S i sempre passa pel punt (0, 0.5).
Regressió Logística: Un model de classificació que utilitza una combinació lineal de les característiques seguida d’una funció sigmoide per predir la probabilitat de pertànyer a una classe.
Frontera de Decisió
La frontera de decisió és la línia (o hiperpla en dimensions superiors) que separa les regions on el model prediu diferents classes.
Una regla comuna és:
\hat y = \begin{cases} 1, & \text{si } g(z) > 0.5,\\ 0, & \text{si no.} \end{cases}
I com que g(z)>0.5 equival a z>0, es pot escriure directament:
\hat y = 1 \quad\Longleftrightarrow\quad w^T x + b \ge 0.
La frontera de decisió és el conjunt de tots els punts x per als quals el model està exactament al límit entre la classe 0 i la classe 1, és a dir, on
z = w^T x + b = 0.
Si x = (x_1, x_2) i w = (w_1, w_2), la frontera de decisió es descriu per
w_1\ x_1 + w_2\ x_2 + b = 0.
Geomètricament, això és una línia al pla (x_1,x_2).
Tots els punts d’un costat (on w_1x_1 + w_2x_2 + b > 0) es classifiquen com a 1, i els de l’altre costat com a 0.
Fronteres no lineals
Per obtenir corbes (cercles, el·lipses, etc.), podem ampliar el vector d’atributs amb termes polinòmics.
Si en lloc de (x_1, x_2) utilitzem \bigl(x_1^2, x_2^2\bigr) i aprenem pesos w_1, w_2 i biaix b, la frontera és
w_1,x_1^2 + w_2,x_2^2 + b = 0.
En el cas simètric senzill amb w_1 = w_2 = 1, això es tradueix en
x_1^2 + x_2^2 = -b,
que és un cercle de radi \sqrt{-b}.
Afegint pesos diferents o termes creuats es poden generar el·lipses rotades, paraboles o formes molt complexes, segons el grau i la combinació de termes polinòmics.
Funció de cost
Per a classificació, no podem usar l’error quadràtic (no funcionaria bé). En lloc d’això, s’utilitza la pèrdua logarítmica (entropia creuada):
L(f_{w,b}(x_i), y_i) = \begin{cases} -\log(f_{w,b}(x_i)) & \text{si } y_i = 1 \\ -\log(1 - f_{w,b}(x_i)) & \text{si } y_i = 0 \end{cases}
Per què aquesta funció? Penalitza molt les prediccions molt segures però incorrectes. Si el model està molt segur que és classe 1 (probabilitat alta) però en realitat és classe 0, el cost és enorme. Això força el model a aprendre millor.
Aquesta funció també garanteix que podem aplicar descens del gradient de forma eficient (és convexa).
Quan l’etiqueta real és 1 (corba verda):
- Si \hat{y} s’apropa a 1 → cost baix (la predicció és correcta i segura)
- Si \hat{y} s’apropa a 0 → cost molt alt (la predicció és completament errònia)
Quan l’etiqueta real és 0 (corba vermella):
- Si \hat{y} s’apropa a 0 → cost baix (predicció correcta)
- Si \hat{y} s’apropa a 1 → cost alt (predicció incorrecta)
Taxa d’encerts (accuracy)
Quan entrenem un model de classificació, necessitem una manera de saber fins a quin punt encerta les prediccions.
La taxa d’encerts (accuracy) és la mesura més senzilla i habitual.
\text{Accuracy} = \frac{\text{Nombre de prediccions correctes}}{\text{Nombre total de prediccions}}
En altres paraules, indica el percentatge de casos en què el model encerta la classe respecte al total de mostres.
Per exemple, si tenim 100 exemples i el model classifica correctament 90 d’ells, l’accuracy és:
\frac{90}{100} = 0.9 (90\%)
Limitació important: Datasets desbalancejats:
Un dataset desbalancejat és aquell on les classes no tenen una distribució similar. Per exemple:
- Detecció de frau: 99% transaccions normals, 1% fraudulentes
- Diagnòstic de malaltia rara: 98% pacients sans, 2% malalts
En aquests casos, l’accuracy pot ser molt enganyosa. Un model que sempre prediu “no frau” tindria 99% d’accuracy però seria completament inútil, ja que no detectaria cap frau real.
Per això, en problemes amb classes desbalancejades cal utilitzar mètriques més adequades com Precision, Recall i F1-Score, que avaluen millor la capacitat del model per detectar la classe minoritària.
Nota: Per a una anàlisi completa de com treballar amb datasets desbalancejats i les mètriques adequades, consulta la secció Classes Desbalancejades a la guia de mètriques d’avaluació.
Regressió logística amb Scikit-learn
import numpy as np
from sklearn.linear_model import LogisticRegression
X = np.array([[0.5, 1.5], [1,1], [1.5, 0.5], [3, 0.5], [2, 2], [1, 2.5]])
y = np.array([0, 0, 0, 1, 1, 1])
# Create and fit a logistic regression model
lr_model = LogisticRegression()
lr_model.fit(X, y)
# Predict on the training set
y_pred = lr_model.predict(X)
print("Prediction on training set:", y_pred)
# Calculate and print the accuracy on the training set
print("Accuracy on training set:", lr_model.score(X, y))

Sobreajustament i Subajustament
Hi ha dos problemes que poden produir-se quan estem entrenant el nostre model:
- Subajustament: model massa simple; alt biaix: el model no capta bé els patrons de les dades.
- Sobreajustament: massa complex; alta variància: model massa sensible a les dades.
Analogia: És com estudiar per un examen. El subajustament és quan no estudies prou i suspens. El sobreajustament és com memoritzar només les preguntes d’exàmens anteriors: encerts perfectament aquelles, però suspens amb preguntes noves perquè no has entès realment els conceptes.
Exemple concret: Imagina que entrenem un model amb un polinomi de grau 10 amb només 5 punts de dades. El model pot passar exactament per tots els punts d’entrenament (error zero!), però la corba serà extremadament tortuosa. Quan arriba una dada nova que no és exactament com les d’entrenament, la predicció serà completament errònia. El model ha “memoritzat” les dades en lloc d’aprendre el patró subjacent.
El sobreajustament implica alta precisió en entrenament però mala generalització.
Com evitar el sobreajustament:
- Més dades d’entrenament.
- Simplificar el model (menys característiques o polinomis més simples).
- Regularització: penalitza pesos grans i redueix la complexitat.
- Exemple: regularització L2 afegeix \frac{\lambda}{2m} \sum_{j=1}^{n} w_j^2 a la funció de cost (tant per regressió com per classificació).
Regularització
La regularització és com posar límits de velocitat al model per evitar que vagi massa ràpid en qualsevol direcció.
Idea bàsica:
- Afegim una “penalització” quan els pesos w són massa grans
- Això força el model a ser més simple i suau
- Un model més simple generalitza millor a dades noves
Analogia: És com conduir amb limitadors de velocitat. Pots arribar al destí, però sense fer maniobres brusques que et facin sortir de la carretera (sobreajustament).
Què penalitzem?
- Penalitzem tots els pesos del model per igual
- Això evita que el model doni massa importància a una sola característica
- El resultat és un model més equilibrat i robust
Com funciona: Funció de cost amb regularització
Afegim un terme extra a la funció de cost:
J(w, b) = \text{Error original} + \text{Penalització pels pesos grans}
Matemàticament:
J(w, b) = J_\text{original}(w, b) + \frac{\lambda}{2m} \sum_{j=1}^{n} w_j^2
Components:
- \lambda (lambda): controla quanta penalització apliquem
- La suma \sum w_j^2 : penalitza els pesos grans (els eleva al quadrat)
- Normalment no penalitzem b (el biaix)
Ajustar \lambda : el paràmetre clau
El valor de \lambda determina l’equilibri entre ajustar-se a les dades i mantenir el model simple:
- \lambda \approx 0 : gairebé sense regularització → risc de sobreajustament
- \lambda moderat: equilibri òptim → bon ajust i bona generalització
- \lambda massa gran: model massa simple → pot donar sempre la mateixa predicció (subajustament)
L’objectiu amb regularització:
- Minimitzar l’error en les dades d’entrenament
- Mantenir els pesos petits per evitar complexitat excessiva
\lambda és qui controla aquest equilibri.
💡 Com escollim el millor λ? Podem usar un conjunt de validació per trobar el valor òptim de λ sense “fer trampes” amb les dades de test. Per obtenir una estimació més robusta, podem utilitzar K-Fold Cross-Validation.
Descens del Gradient amb Regularització
Amb regularització, l’actualització dels pesos té un terme addicional:
w_j := w_j - \alpha \left( \text{Gradient normal} + \frac{\lambda}{m} w_j \right)
Què canvia?
- Mantenim el gradient normal (la direcció per reduir l’error)
- Afegim \frac{\lambda}{m} w_j : un terme que “empeny” w_j cap a zero
Efecte pràctic:
- Si un pes no ajuda gaire a reduir l’error, la regularització el farà més petit
- Els pesos només creixeran si realment milloren les prediccions
- Això evita que el model es compliqui innecessàriament
Nota: El biaix b normalment no es regularitza, només els pesos w .
Mètriques d’avaluació
- Mètriques per a regressió
- Mètriques per a classificació
- Taxa d’encerts (Accuracy)
- Matriu de Confusió
- Precision (Precisió)
- Recall (Sensibilitat o Exhaustivitat)
- Trade-off entre Precision i Recall
- F1-Score
- Log Loss (Entropia creuada)
- El Llindar de Decisió
- Corba ROC-AUC
- Interpretació de la corba ROC
- Corba Precision-Recall
- ROC-AUC vs Precision-Recall: Quan usar cada una
- Classes Desbalancejades
- Exemple Complet: Detecció de Frau
- Implementació amb Python
- Taula Resum de Totes les Mètriques
- Conclusions Finals
- Extensió a classificació multiclasse
- Recomanacions pràctiques
Mètriques per a regressió
Un cop hem entrenat el nostre model de regressió, necessitem avaluar com de bé funciona. La funció de cost J(w, b) ens serveix per entrenar el model, però per avaluar-ne la qualitat utilitzem mètriques més interpretables.
R² (coeficient de determinació)
El R² (R-squared o coeficient de determinació) és la mètrica més popular per avaluar models de regressió. Ens diu quina proporció de la variabilitat de les dades és explicada pel model.
R^2 = 1 - \frac{\sum_{i=1}^m (y_i - \hat{y}_i)^2}{\sum_{i=1}^m (y_i - \bar{y})^2}
On:
- ŷᵢ: Predicció del model per a l’exemple i
- ȳ: Mitjana de tots els valors reals
- Numerador: Suma dels errors quadrats del model
- Denominador: Variància total de les dades
Interpretació:
R² = 1 - (variància no explicada pel model / variància total de les dades)
= (variància explicada pel model / variància total de les dades)
- R² = 1: El model explica perfectament totes les dades (prediccions perfectes)
- R² = 0.8: El model explica el 80% de la variabilitat (molt bo)
- R² = 0.5: El model explica el 50% de la variabilitat (acceptable)
- R² = 0: El model no explica res millor que predir sempre la mitjana
- R² < 0: El model és pitjor que predir simplement la mitjana (molt dolent)
Avantatges:
- Fàcil d’interpretar (típicament entre 0 i 1)
- Independent de l’escala de les dades
- Molt utilitzat en la pràctica
Limitacions:
- Sempre millora (o es manté igual) quan afegim més variables, fins i tot si no són útils
- Pot ser enganyós amb models molt complexos
MSE (error quadràtic mitjà)
El MSE (Mean Squared Error) mesura la mitjana dels errors al quadrat entre les prediccions i els valors reals.
\text{MSE} = \frac{1}{m} \sum_{i=1}^m (y_i - \hat{y}_i)^2
Nota: És molt similar a la funció de cost J(w, b), però sense el factor 1/2.
Interpretació:
- MSE baix: Prediccions properes als valors reals (bon model)
- MSE alt: Prediccions llunyanes dels valors reals (mal model)
- Les unitats són el quadrat de les unitats originals (ex: si y és en euros, MSE és en euros²)
Avantatges:
- Matemàticament convenient (diferenciable)
- Penalitza fortament els errors grans (per l’elevació al quadrat)
Limitacions:
- Difícil d’interpretar per les unitats al quadrat
- Molt sensible a outliers (valors extrems)
Exemple: Si predim preus de cases i MSE = 10000 euros², això vol dir que l’error típic elevat al quadrat és 10000.
RMSE (arrel de l’error quadràtic mitjà)
El RMSE (Root Mean Squared Error) és simplement l’arrel quadrada del MSE, la qual cosa el fa més fàcil d’interpretar.
\text{RMSE} = \sqrt{\text{MSE}} = \sqrt{\frac{1}{m} \sum_{i=1}^m (y_i - \hat{y}_i)^2}
Interpretació:
- Representa l’error típic en les mateixes unitats que la variable objectiu
- RMSE = 5000 euros significa que, de mitjana, les prediccions s’equivoquen en uns 5000 euros
Avantatges:
- Mateixes unitats que la variable original (molt més interpretable que MSE)
- Encara penalitza els errors grans
- Mètrica molt utilitzada en competicions i en la pràctica
Limitacions:
- Com MSE, és sensible a outliers
- No té una interpretació percentual com R²
Quan usar-lo: RMSE és excel·lent quan vols saber “de mitjana, quant m’equivoco?” en les unitats originals del problema.
MAE (error absolut mitjà)
El MAE (Mean Absolute Error) mesura la mitjana dels errors absoluts (sense elevar al quadrat).
\text{MAE} = \frac{1}{m} \sum_{i=1}^m |y_i - \hat{y}_i|
Interpretació:
- Representa l’error mitjà absolut en les mateixes unitats que la variable objectiu
- MAE = 3000 euros significa que, de mitjana, les prediccions s’equivoquen en 3000 euros (en valor absolut)
Avantatges:
- Molt intuïtiu: “error mitjà absolut”
- Mateixes unitats que la variable original
- Menys sensible a outliers que MSE/RMSE (no eleva al quadrat)
- Tracta tots els errors per igual
Limitacions:
- No penalitza tant els errors grans com MSE/RMSE
- Menys convenient matemàticament (no és diferenciable a zero)
Diferència clau amb RMSE:
- RMSE castiga més els errors grans (per l’elevació al quadrat)
- MAE tracta tots els errors de forma més uniforme
Taula comparativa de mètriques
| Mètrica | Unitats | Sensibilitat a outliers | Interpretació | Quan usar-la |
|---|---|---|---|---|
| R² | Sense unitats (0-1) | Mitjana | % de variabilitat explicada | Comparar models, veure quant explica el model |
| MSE | Unitats² | Molt alta | Error quadràtic mitjà | Optimització matemàtica, funcions de cost |
| RMSE | Unitats originals | Alta | Error típic | Quan vols saber l’error típic en unitats originals |
| MAE | Unitats originals | Baixa | Error mitjà absolut | Quan hi ha outliers o vols tractar errors uniformement |
Exemple pràctic
Imaginem que predim preus de cases amb tres models diferents:
Model A: R² = 0.85, RMSE = 25000€, MAE = 18000€
Model B: R² = 0.82, RMSE = 30000€, MAE = 15000€
Model C: R² = 0.90, RMSE = 20000€, MAE = 16000€
Interpretació:
- Model C: Millor R² (explica el 90% de la variabilitat) i millor RMSE
- Model B: Pitjor RMSE però millor MAE, suggereix que potser té alguns errors grans però menys errors petits
- Model A: Equilibrat, però no el millor en cap mètrica
Conclusió: Model C sembla el millor en general. Si hi ha molts outliers problemàtics, podríem considerar Model B.
Com Calcular-les amb Python
import numpy as np
from sklearn.metrics import r2_score, mean_squared_error, mean_absolute_error
# Valors reals i prediccions
y_true = np.array([100, 150, 200, 250, 300])
y_pred = np.array([110, 145, 195, 260, 290])
# Calcular mètriques
r2 = r2_score(y_true, y_pred)
mse = mean_squared_error(y_true, y_pred)
rmse = np.sqrt(mse) # o mean_squared_error(y_true, y_pred, squared=False)
mae = mean_absolute_error(y_true, y_pred)
print(f"R² = {r2:.4f}")
print(f"MSE = {mse:.2f}")
print(f"RMSE = {rmse:.2f}")
print(f"MAE = {mae:.2f}")
Recomanacions pràctiques
Sempre avalua amb múltiples mètriques per obtenir una visió completa del rendiment del model:
- R² → Visió general: Quin % de variabilitat explica el model?
- RMSE → Error típic en unitats originals: De mitjana, quant m’equivoco?
- MAE → Error absolut mitjà: Tracta tots els errors per igual
Regla d’or: Compara RMSE vs MAE per detectar problemes:
- Si RMSE >> MAE: Tens outliers que distorsionen les prediccions
- Si RMSE ≈ MAE: Els errors són uniformes i el model és estable
Quina mètrica prioritzar segons el context?
- Presentar resultats a clients/stakeholders → R² (fàcil d’interpretar: “explico el 85% de la variabilitat”)
- Entendre l’error típic del negoci → RMSE (“m’equivoco una mitjana de 5.000€”)
- Dades amb valors extrems (outliers) → MAE (menys sensible a extrems)
- Entrenar/optimitzar el model → MSE (matemàticament convenient)
Workflow recomanat:
- Calcula totes les mètriques (R², RMSE, MAE, MSE)
- Usa R² per avaluar si el model té sentit (> 0.7 és generalment bo)
- Usa RMSE/MAE per quantificar l’error en termes del negoci
- Compara RMSE/MAE per identificar si els outliers són un problema
Mètriques per a classificació
Quan entrenem un model de classificació, necessitem mètriques que ens diguin com de bé funciona. A diferència de la regressió, aquí no predim números sinó categories, per tant necessitem mètriques diferents.
Taxa d’encerts (Accuracy)
La taxa d’encerts (accuracy) és la mesura més senzilla i intuïtiva.
\text{Accuracy} = \frac{\text{Nombre de prediccions correctes}}{\text{Nombre total de prediccions}}
En altres paraules, indica el percentatge de casos en què el model encerta la classe respecte al total de mostres.
Per exemple, si tenim 100 exemples i el model classifica correctament 90 d’ells, l’accuracy és:
\frac{90}{100} = 0.9 (90\%)
Limitacions de l’Accuracy:
L’accuracy pot ser molt enganyosa en situacions comunes:
Exemple pràctic: Imagina que vols detectar una malaltia rara que només afecta l’1% de la població. Si el teu model sempre prediu “no malalt”, tindrà un 99% d’accuracy! Però el model és completament inútil perquè no detecta cap cas real de la malaltia.
Per això necessitem mètriques més sofisticades que distingeixin entre tipus d’errors.
Matriu de Confusió
La matriu de confusió és la base per entendre totes les altres mètriques de classificació. Mostra els quatre tipus possibles de resultats:
| Predicció: Positiu | Predicció: Negatiu | |
|---|---|---|
| Real: Positiu | TP (True Positive) | FN (False Negative) |
| Real: Negatiu | FP (False Positive) | TN (True Negative) |
Definicions:
- TP (Veritables Positius): Casos positius que hem identificat correctament
- TN (Veritables Negatius): Casos negatius que hem identificat correctament
- FP (Falsos Positius): Casos negatius que hem classificat erròniament com a positius (Error Tipus I)
- FN (Falsos Negatius): Casos positius que hem classificat erròniament com a negatius (Error Tipus II)
Exemple mèdic: En una prova de càncer:
- TP: Detectem càncer i realment el té
- TN: Diem que no té càncer i realment no en té
- FP: Diem que té càncer però no en té (falsa alarma, ansietat innecessària)
- FN: Diem que no té càncer però sí que en té (molt perillós! tractament tardà)
\text{Accuracy} = \frac{TP + TN}{TP + TN + FP + FN}
Precision (Precisió)
La precision respon a la pregunta: “Dels casos que he predit com a positius, quants ho són realment?”
\text{Precision} = \frac{TP}{TP + FP}
Interpretació:
- Precision alta: Quan dic que és positiu, gairebé sempre encerto
- Precision baixa: Faig moltes falses alarmes (falsos positius)
Quan és important? Quan els falsos positius són costosos.
Exemples:
- Sistema de spam: Si la precision és baixa, molts correus importants aniran a spam (molt molest!)
- Sistema de recomanació de productes: Si la precision és baixa, recomanem productes que no interessen (mala experiència d’usuari)
Recall (Sensibilitat o Exhaustivitat)
El recall respon a la pregunta: “De tots els casos que són realment positius, quants n’he detectat?”
\text{Recall} = \frac{TP}{TP + FN}
Interpretació:
- Recall alt: Detecto quasi tots els casos positius reals
- Recall baix: Em deixo molts casos positius sense detectar (falsos negatius)
Quan és important? Quan els falsos negatius són costosos o perillosos.
Exemples:
- Diagnòstic de malalties greus: Recall baix significa que no detectem malalts reals (molt perillós!)
- Detecció de frau: Recall baix significa que deixem passar fraus reals (pèrdues econòmiques)
- Sistemes de seguretat: Recall baix significa que no detectem amenaces reals
Trade-off entre Precision i Recall
Hi ha un compromís inevitable entre precision i recall:
-
Model molt estricte (prediu positiu només si està MOLT segur):
- Precision alta (poques falses alarmes)
- Recall baix (es deixa casos positius)
-
Model molt permissiu (prediu positiu amb poca evidència):
- Recall alt (detecta gairebé tots els positius)
- Precision baixa (moltes falses alarmes)
Analogia: És com un detector de metalls a l’aeroport:
- Molt sensible: Detectarà tot (recall alt) però sonarà constantment amb petits objectes (precision baixa)
- Poc sensible: Només sonarà amb objectes grans (precision alta) però es pot deixar coses perilloses (recall baix)
F1-Score
El F1-Score és la mitjana harmònica de precision i recall, combinant ambdues mètriques en un sol número.
\text{F1} = 2 \times \frac{\text{Precision} \times \text{Recall}}{\text{Precision} + \text{Recall}} = \frac{2 \times TP}{2 \times TP + FP + FN}
Per què mitjana harmònica? Penalitza desequilibris. Si una mètrica és molt baixa, el F1 també serà baix, encara que l’altra sigui alta.
Nota: L’F1 score és equivalent a la mitjana harmònica de precisió i recall.
Exemple:
- Precision = 0.9, Recall = 0.9 → F1 = 0.90 (excel·lent i equilibrat)
- Precision = 1.0, Recall = 0.1 → F1 = 0.18 (desequilibrat, F1 baix)
- Precision = 0.5, Recall = 0.5 → F1 = 0.50 (equilibrat però mediocre)
Quan usar F1?
- Quan vols un equilibri entre precision i recall
- Quan ambdós tipus d’error són igualment importants
- Com a mètrica única per comparar models
Variants: F2-Score (dóna més pes al recall), F0.5-Score (dóna més pes a la precision)
Log Loss (Entropia creuada)
El Log Loss és l’equivalent a MSE per a classificació. Mesura com de bones són les probabilitats que prediu el model, no només si encerta la classe.
\text{Log Loss} = -\frac{1}{m} \sum_{i=1}^m \left[ y_i \log(\hat{y}_i) + (1-y_i) \log(1-\hat{y}_i) \right]
On:
- ŷᵢ: Probabilitat predita que l’exemple i sigui de la classe 1
- yᵢ: Classe real (0 o 1)
Nota: Aquesta fórmula és per a classificació binària. Per a multiclasse, s’utilitza la generalització categorical cross-entropy.
Interpretació:
- Log Loss baix: El model dóna probabilitats precises
- Log Loss alt: El model dóna probabilitats errònies o és molt insegur
Diferència amb Accuracy:
- Dos models poden tenir la mateixa accuracy però Log Loss diferent
- Un model que prediu probabilitats de 0.51 vs 0.99 per a la classe correcta tindrà accuracy igual però Log Loss molt diferent
Exemple:
- Model A: Prediu 0.99 de probabilitat i és correcte → Log Loss molt baix
- Model B: Prediu 0.51 de probabilitat i és correcte → Log Loss més alt
- Ambdós encerten (accuracy igual) però Model A és més “segur” i millor
Per què és important? Moltes aplicacions necessiten probabilitats bones, no només prediccions:
- Recomanació de productes (ordenar per probabilitat)
- Diagnòstic mèdic (necessites saber quant de segur està el model)
- Sistemes de presa de decisions
El Llindar de Decisió
Abans d’entendre les corbes ROC i Precision-Recall, cal comprendre un concepte clau: el llindar de decisió.
Què és el llindar de decisió? Quan un model de classificació binària fa una predicció, normalment retorna una probabilitat entre 0 i 1. Per convertir aquesta probabilitat en una predicció de classe, utilitzem un llindar (threshold):
- Si
probabilitat >= llindar→ Predim classe Positiva (1) - Si
probabilitat < llindar→ Predim classe Negativa (0)
El llindar més comú és 0.5, però no sempre és òptim per a tots els problemes.
Per què importa el llindar?
Canviar el llindar afecta directament al trade-off entre Precision i Recall, i per tant al rendiment global del model.
Exemple pràctic:
# Probabilitats predites pel model
probabilitats = [0.9, 0.7, 0.45, 0.3, 0.1]
# Amb llindar = 0.5 (estàndard)
prediccions_05 = [1, 1, 0, 0, 0] # Només els 2 primers són positius
# Amb llindar = 0.4 (més permissiu)
prediccions_04 = [1, 1, 1, 0, 0] # Ara també el tercer és positiu
# Amb llindar = 0.8 (més estricte)
prediccions_08 = [1, 0, 0, 0, 0] # Només el primer és positiu
Tant la corba ROC com la corba Precision-Recall avaluen el model a través de tots els possibles llindars, oferint una visió completa del seu rendiment.
Corba ROC-AUC
La corba ROC (Receiver Operating Characteristic) i l’AUC (Area Under the Curve) mesuren la capacitat del model per discriminar entre classes a través de tots els possibles llindars de decisió.
Com funciona la corba ROC
La corba ROC visualitza el rendiment del model representant:
-
Eix Y (TPR - True Positive Rate): També conegut com Recall o Sensibilitat \text{TPR} = \frac{TP}{TP + FN} = \text{Recall} Mesura: “De tots els positius reals, quin % detectem?”
-
Eix X (FPR - False Positive Rate): Taxa de falsos positius \text{FPR} = \frac{FP}{FP + TN} Mesura: “De tots els negatius reals, quin % classifiquem erròniament com a positius?”
Interpretació de la corba ROC
Punts clau:
- Línia diagonal (AUC=0.5): Model aleatori, equivalent a llançar una moneda
- Corba cap amunt esquerra (AUC>0.5): Millor que l’atzar
- Línia superior esquerra (AUC=1.0): Model perfecte (TPR=1, FPR=0)
Què és l’AUC?
L’AUC (Area Under the Curve) és l’àrea sota la corba ROC, un valor entre 0 i 1 que resumeix el rendiment global del model.
Interpretació intuïtiva: L’AUC representa la probabilitat que el model assigni una probabilitat més alta a un exemple positiu aleatori que a un exemple negatiu aleatori.
Per exemple, AUC = 0.85 significa que, si agafes un cas positiu i un negatiu a l’atzar, el model assignarà correctament una probabilitat més alta al positiu en el 85% dels casos.
Avantatges del ROC-AUC:
- Independent del llindar: Avalua el model a través de tots els llindars possibles
- Fàcil de comparar models: Un sol número (AUC) per comparar
- Visualització intuïtiva: La corba mostra clarament el trade-off entre TPR i FPR
- Permet ajustar el llindar: Pots escollir el llindar òptim segons el context
- Considera ambdues classes: Té en compte tant verdaders positius com verdaders negatius
Limitacions del ROC-AUC
-
Pot ser optimista amb classes molt desbalancejades: Si tens 99% de negatius i 1% de positius, ROC-AUC pot donar una falsa sensació de bon rendiment perquè el FPR es calcula sobre molts negatius
-
No indica el millor llindar: Només mostra el rendiment a tots els llindars, però no diu quin usar. Cal decidir el llindar segons el cost dels errors (FP vs FN)
-
Només per a classificació binària (o per parelles de classes en multiclasse)
-
No distingeix entre tipus d’errors: Un FP i un FN compten igual, però sovint tenen costos diferents
-
Pot amagar problemes amb dades desbalancejades: El gran nombre de TN pot “inflar” artificialment la mètrica
Usar ROC-AUC quan:
- Vols comparar diferents models de forma global
- No estàs segur de quin llindar utilitzar
- Les classes estan relativament balancejades (ex: 30%-70% o millor)
- T’interessa el rendiment global sobre ambdues classes
- Vols una mètrica robusta i independent del llindar
NO usar ROC-AUC quan:
- Classes extremadament desbalancejades (ex: 1%-99%) → usar Precision-Recall
- Necessites saber el rendiment a un llindar específic → usar Precision/Recall/F1
- Els costos de FP i FN són molt diferents → usar mètriques de cost
- El focus està només en la classe positiva → usar Precision-Recall
Corba Precision-Recall
La corba Precision-Recall mostra el rendiment d’un model binari a través de tots els possibles llindars, centrant-se només en la classe positiva. És especialment útil quan les dades estan desequilibrades (pocs positius).
La corba Precision-Recall visualitza el rendiment del model representant:
-
Eix Y (Precision - Precisió)
\text{Precision} = \frac{TP}{TP + FP} Mesura: “De tots els que hem classificat com a positius, quin % ho són realment?” -
Eix X (Recall - Exhaustivitat o Sensibilitat)
\text{Recall} = \frac{TP}{TP + FN} Mesura: “De tots els positius reals, quin % hem detectat?”
Interpretació de la corba Precision-Recall:
- Corba alta cap amunt dreta → millor rendiment (alta precisió i alt recall)
- Corba baixa o plana → el model genera molts falsos positius o no detecta prou positius
- El punt (Recall=1, Precision=proporció de positius) → línia de base d’un model aleatori
Què és l’AP (Average Precision)?
L’Average Precision (AP) és l’àrea sota la corba Precision-Recall, anàloga a l’AUC de la corba ROC. És un valor entre 0 i 1 que resumeix el rendiment global del model.
Interpretació intuïtiva: La corba PR mostra com de bé el model identifica positius sense “embrutar-se” amb massa falsos positius:
- Si la precisió cau ràpidament quan augmenta el recall, el model confon molts negatius amb positius
- Si la corba es manté alta, el model detecta positius amb confiança
Avantatges de Precision-Recall:
- Focus en la classe positiva: Només considera TP, FP i FN. Ignora els TN, que sovint són la majoria
- Millor per classes desbalancejades: No es veu “inflada” per un gran nombre de verdaders negatius
- Més sensible a canvis: Reflecteix millor millores en la detecció de positius
- Rellevant per molts casos reals: En frau, diagnòstic, etc., ens importa més detectar positius que evitar falses alarmes
- Independent del llindar: Com ROC, avalua tots els llindars possibles
Limitacions de Precision-Recall:
- Més difícil d’interpretar: Menys intuïtiva que ROC per a principiants
- No hi ha baseline universal clar: La línia de referència depèn de la proporció de positius al dataset
- Varia més entre datasets: Més sensible a la distribució de classes que ROC-AUC
- Només per a classificació binària (o per parelles de classes en multiclasse)
- Ignora els verdaders negatius: No té en compte com de bé classifiquem els negatius
Usar Precision-Recall quan:
- Les classes estan molt desbalancejades (ex: 1%-99%, 5%-95%)
- T’importa principalment la classe positiva minoritària
- Els falsos positius i falsos negatius tenen costos similars
- Vols una visió clara de com detectes la classe d’interès
- Treballes amb problemes com: frau, malalties rares, detecció d’anomalies
NO usar Precision-Recall quan:
- Les classes estan balancejades → usar ROC-AUC
- T’interessa el rendiment sobre ambdues classes per igual
- Necessites una mètrica més estandarditzada per comparar amb literatura
ROC-AUC vs Precision-Recall: Quan usar cada una
Regla pràctica ràpida:
Proporció de classes → Mètrica recomanada
- Balancejades (40-60%) → ROC-AUC
- Lleument desbalancejades (20-80%) → ROC-AUC o ambdues
- Moderadament desbalancejades (10-90%) → Precision-Recall (prioritària)
- Molt desbalancejades (<5% positius) → Precision-Recall (obligatòria)
Taula comparativa:
| Aspecte | ROC-AUC | Precision-Recall |
|---|---|---|
| Focus | Ambdues classes (positius i negatius) | Només classe positiva |
| Millor per | Classes balancejades | Classes desbalancejades |
| Sensibilitat a TN | Sí (pot inflar-se) | No (ignora TN) |
| Interpretació | Més intuïtiva | Menys intuïtiva |
| Baseline | 0.5 (línia diagonal) | Proporció de positius |
| Optimisme en desbalanceig | Sí, pot ser optimista | No, més realista |
Casos d’ús específics:
Usar ROC-AUC:
- Diagnòstic mèdic amb prevalença normal (~20-50%)
- Classificació de sentiment (positiu/negatiu)
- Detecció de spam en entorns equilibrats
- A/B testing amb conversions normals
Usar Precision-Recall:
- Detecció de frau (0.1%-2% de casos)
- Malalties rares (<1% de prevalença)
- Detecció d’anomalies en sistemes
- Clics en publicitat (CTR baix)
- Recuperació d’informació (pocs documents rellevants)
Consell: En casos dubtosos, calcula i visualitza ambdues corbes. Ofereixen perspectives complementàries.
Classes Desbalancejades
Quan treballem en aplicacions de machine learning on la proporció d’exemples positius i negatius és molt desigual (per exemple, molt lluny d’un repartiment 50-50), les mètriques habituals com la taxa d’encerts (accuracy) no són adequades.
La taxa d’encerts es defineix com el nombre de prediccions correctes respecte del total. Per exemple: imaginem que entrenem un classificador binari per detectar una malaltia rara a partir de proves mèdiques.
- Definim y = 1 si el pacient té la malaltia (cas rar), i y = 0 altrament.
- Suposem que obtenim un 1% d’error al conjunt de test, és a dir, una taxa d’encerts del 99%.
Sembla un gran resultat, però si només 0,5% dels pacients tenen realment la malaltia, un programa trivial que sempre prediu y = 0 ja aconseguiria una taxa d’encerts del 99,5%. Aquesta estratègia, tot i ser inútil en la pràctica, superaria el nostre model.
Això mostra que l’accuracy no és fiable en situacions amb dades desbalancejades.
Per què és problemàtic?
-
L’accuracy és enganyosa: Un model que sempre prediu la classe majoritària té alta accuracy però és inútil
Exemple: Si sempre prediuem “no frau” en un dataset amb 99% de transaccions legítimes:
- Accuracy = 99% ✓ (sembla excel·lent!)
- Recall = 0% ✗ (no detecta cap frau!)
- F1 = 0 ✗ (el model és inútil)
-
El model tendeix a ignorar la classe minoritària: Aprèn que “sempre dir negatiu” minimitza l’error global
-
ROC-AUC pot ser massa optimista: El gran nombre de TN pot amagar problemes en la detecció de positius
Impacte en ROC vs Precision-Recall
ROC-AUC amb classes desbalancejades:
- El FPR es calcula sobre molts negatius: FPR = FP / (FP + TN)
- Fins i tot molts FP poden resultar en un FPR baix
- Pot donar ROC-AUC = 0.85 quan el model és poc útil en la pràctica
Precision-Recall amb classes desbalancejades:
- La Precision només considera positius: Precision = TP / (TP + FP)
- No es veu afectada pel gran nombre de TN
- Reflecteix millor si el model és útil per detectar la classe positiva
Solucions pràctiques:
-
Usar mètriques adequades:
- No usar accuracy
- Usar Precision-Recall i Average Precision
- Usar F1-Score (equilibra Precision i Recall)
- Usar ROC-AUC amb precaució, complementant amb PR
-
Balancejar les dades:
- Oversampling: Duplicar exemples de la classe minoritària
- Undersampling: Reduir exemples de la classe majoritària
- SMOTE: Generar exemples sintètics de la classe minoritària
-
Ajustar pesos de classe:
- Donar més pes a la classe minoritària en la funció de cost
- Exemple en scikit-learn:
class_weight='balanced'
-
Ajustar el llindar de decisió:
- En lloc de 0.5, usar un llindar més baix (ex: 0.3) per detectar més positius
- Analitzar la corba PR per trobar el llindar òptim segons el context
Exemple Complet: Detecció de Frau
Imaginem un model per detectar transaccions fraudulentes on només l’1% són fraus reals.
Context del problema:
- Dataset: 10,000 transaccions
- Positius (fraus): 100 transaccions (1%)
- Negatius (legítimes): 9,900 transaccions (99%)
Compararem dos models, el naïf i l’entrenat adequadament.
Model A: Predictor naïf (sempre prediu “no frau”)
Prediccions: Totes = 0 (negatiu)
Matriu de confusió:
Predició
Neg Pos
Real Neg [9900 0]
Pos [ 100 0]
TP=0, TN=9900, FP=0, FN=100
Mètriques:
- Accuracy: 9900/10000 = 99% ✓ (sembla excel·lent!)
- Precision: 0/0 = undefined (no prediu cap frau)
- Recall: 0/100 = 0% ✗ (no detecta cap frau)
- F1-Score: 0 ✗
- ROC-AUC: 0.50 (aleatori, perquè no discrimina res)
Conclusió: Accuracy diu que és excel·lent, però el model és completament inútil. No detecta cap frau!
Model B: Model entrenat adequadament
Matriu de confusió (llindar = 0.5):
Predició
Neg Pos
Real Neg [9700 200]
Pos [ 40 60]
TP=60, TN=9700, FP=200, FN=40
Mètriques:
- Accuracy: 9760/10000 = 97.6% (sembla pitjor que Model A?)
- Precision: 60/260 = 23% (de 260 alertes, 60 són fraus reals)
- Recall: 60/100 = 60% (detecta 60 dels 100 fraus)
- F1-Score: 0.33
- ROC-AUC: 0.92 (excel·lent discriminació)
- Average Precision: 0.65 (bon rendiment en classe positiva)
Conclusió: Accuracy més baixa, però el model és infinitament més útil. Detecta 60% dels fraus amb poques falses alarmes.
Per què ROC-AUC pot enganyar aquí?
Model B té ROC-AUC = 0.92, que sembla excel·lent. Però:
- FPR = 200/(200+9700) = 2% (molt baix perquè hi ha molts negatius)
- TPR = 60/100 = 60%
El gran nombre de TN (9700) fa que el FPR sigui baixíssim fins i tot amb 200 FP, resultant en una ROC-AUC alta que pot ser massa optimista.
Per què Precision-Recall és millor aquí?
La corba PR mostra la realitat:
- Precision = 23% (només 1 de cada 4 alertes és frau real)
- Recall = 60%
- Average Precision = 0.65
Aquesta mètrica és més honesta sobre el rendiment real: el model és útil però no perfecte, i genera moltes falses alarmes relatives als fraus detectats.
En problemes amb classes molt desbalancejades, mai confiïs només en Accuracy o ROC-AUC. Usa sempre Precision-Recall per tenir una visió realista del rendiment.
Implementació amb Python
Aquí tens un exemple complet que mostra com calcular i visualitzar ambdues corbes:
import numpy as np
import matplotlib.pyplot as plt
from sklearn.metrics import (
accuracy_score, precision_score, recall_score, f1_score,
confusion_matrix, log_loss, roc_auc_score, classification_report,
roc_curve, precision_recall_curve, average_precision_score
)
# Dades d'exemple
y_true = np.array([0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0])
y_pred = np.array([0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0])
y_prob = np.array([0.15, 0.82, 0.68, 0.75, 0.58, 0.22, 0.91, 0.34, 0.88, 0.45,
0.62, 0.12, 0.71, 0.48, 0.79, 0.08, 0.53, 0.85, 0.29, 0.19])
# ============================================
# 2. CORBA ROC-AUC
# ============================================
fpr, tpr, thresholds_roc = roc_curve(y_true, y_prob)
roc_auc = roc_auc_score(y_true, y_prob)
plt.figure(figsize=(16, 6))
# Subplot 1: ROC Curve
plt.subplot(1, 2, 1)
plt.plot(fpr, tpr, color='darkorange', lw=3,
label=f'Model (AUC = {roc_auc:.3f})', marker='o', markersize=6)
plt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--',
label='Classificador aleatori (AUC = 0.50)')
plt.xlim([-0.05, 1.05])
plt.ylim([-0.05, 1.05])
plt.xlabel('Taxa de Falsos Positius (FPR)\n← Menys falses alarmes Més falses alarmes →',
fontsize=11)
plt.ylabel('Taxa de Vertaders Positius (TPR / Recall)\n← Menys fraus detectats Més fraus detectats →',
fontsize=11)
plt.title('Corba ROC - Detecció de Frau', fontsize=14, fontweight='bold')
plt.legend(loc="lower right", fontsize=10)
plt.grid(True, alpha=0.3)
# ============================================
# 3. CORBA PRECISION-RECALL
# ============================================
precision, recall, thresholds_pr = precision_recall_curve(y_true, y_prob)
avg_precision = average_precision_score(y_true, y_prob)
# Subplot 2: Precision-Recall Curve
plt.subplot(1, 2, 2)
plt.plot(recall, precision, color='purple', lw=3,
label=f'Model (AP = {avg_precision:.3f})', marker='o', markersize=6)
# Línia baseline (proporció de positius)
baseline = np.sum(y_true) / len(y_true)
plt.plot([0, 1], [baseline, baseline], color='navy', lw=2, linestyle='--',
label=f'Baseline (proporció positius = {baseline:.2f})')
plt.xlim([-0.05, 1.05])
plt.ylim([-0.05, 1.05])
plt.xlabel('Recall (Exhaustivitat)\n← Detectem menys positius Detectem més positius →',
fontsize=11)
plt.ylabel('Precision (Precisió)\n← Més falses alarmes Menys falses alarmes →',
fontsize=11)
plt.title('Corba Precision-Recall - Detecció de Frau', fontsize=14, fontweight='bold')
plt.legend(loc="lower left", fontsize=10)
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Taula Resum de Totes les Mètriques
| Mètrica | Què mesura | Quan usar-la | Sensible a desbalanceig |
|---|---|---|---|
| Accuracy | % d’encerts totals | Classes balancejades, tots els errors igual d’importants | ❌ Sí, molt |
| Precision | Qualitat dels positius predits | Falsos positius costosos (spam, recomanacions) | ✅ No |
| Recall | % de positius detectats | Falsos negatius perillosos (malalties, frau) | ✅ No |
| F1-Score | Equilibri Precision-Recall | Compromís entre ambdós errors | ✅ No |
| Log Loss | Qualitat de les probabilitats | Necessites bones probabilitats, no només classes | ✅ No |
| ROC-AUC | Discriminació global entre classes | Classes balancejades, comparació de models | ⚠️ Parcialment |
| PR-AUC (AP) | Discriminació centrada en positius | Classes desbalancejades, focus en classe positiva | ✅ No |
Conclusions Finals
Punts clau a recordar:
-
El llindar de decisió és fonamental: Canviar-lo afecta tot el rendiment del model
-
ROC-AUC per visió global: Millor quan les classes estan balancejades i t’interessen ambdues classes
-
Precision-Recall per focus en positius: Imprescindible quan les classes estan desbalancejades
-
Accuracy pot enganyar: Mai la usis sola, especialment amb desbalanceig
-
Calcula ambdues corbes en cas de dubte: Ofereixen perspectives complementàries
Workflow recomanat:
- Analitza la proporció de classes al teu dataset
- Si desbalanceig > 80%-20% → Prioritza Precision-Recall
- Si balancejat → Usa ROC-AUC com a principal
- Sempre calcula ambdues corbes per tenir visió completa
- Analitza llindars per trobar el punt òptim segons el teu context
- Decideix el llindar segons el cost dels errors (FP vs FN)
La millor mètrica és la que reflecteix millor els objectius del teu problema real.
Extensió a classificació multiclasse
Totes les mètriques descrites anteriorment per a classificació binària també s’apliquen a classificació multiclasse (més de 2 classes), però amb algunes adaptacions importants.
Mètriques que funcionen igual:
- Accuracy: Es calcula exactament igual →
prediccions_correctes / total_prediccions - Log Loss: S’estén a categorical cross-entropy per múltiples classes
- Matriu de confusió: Passa a ser una matriu N×N (on N és el nombre de classes)
Mètriques que requereixen estratègies d’agregació:
Per a mètriques com Precision, Recall i F1-Score, hem de calcular-les per cada classe individualment i després agregar-les. Existeixen tres estratègies principals:
1. Macro-average (mitjana macro):
- Calcula la mètrica per cada classe i després fa la mitjana
- Tracta totes les classes amb la mateixa importància (independentment del nombre d’exemples)
- Quan usar-la: Quan totes les classes són igualment importants
- Fórmula:
(metric_class1 + metric_class2 + ... + metric_classN) / N
2. Weighted-average (mitjana ponderada):
- Calcula la mètrica per cada classe i fa una mitjana ponderada pel nombre d’exemples de cada classe
- Dóna més importància a les classes amb més exemples
- Quan usar-la: Quan vols reflectir la distribució real de les classes
- Fórmula:
Σ(metric_classi × n_classi) / n_total
3. Micro-average (mitjana micro):
- Agrega primer tots els TP, FP, FN de totes les classes i després calcula la mètrica global
- Dóna més pes a les classes majoritàries
- Quan usar-la: Quan vols optimitzar per al rendiment global
- Per a accuracy: micro = macro = accuracy (són equivalents)
Exemple pràctic:
from sklearn.metrics import classification_report, precision_score, recall_score, f1_score
# Classes: 0=Rosa, 1=Tulipa, 2=Marguerida
y_true = [0, 0, 0, 1, 1, 1, 1, 2, 2, 2]
y_pred = [0, 0, 1, 1, 1, 0, 1, 2, 2, 0]
# Mètriques amb diferents estratègies
print(f"Precision (macro): {precision_score(y_true, y_pred, average='macro'):.4f}")
print(f"Precision (weighted): {precision_score(y_true, y_pred, average='weighted'):.4f}")
print(f"Precision (micro): {precision_score(y_true, y_pred, average='micro'):.4f}")
print(f"\nRecall (macro): {recall_score(y_true, y_pred, average='macro'):.4f}")
print(f"Recall (weighted): {recall_score(y_true, y_pred, average='weighted'):.4f}")
print(f"Recall (micro): {recall_score(y_true, y_pred, average='micro'):.4f}")
print(f"\nF1 (macro): {f1_score(y_true, y_pred, average='macro'):.4f}")
print(f"F1 (weighted): {f1_score(y_true, y_pred, average='weighted'):.4f}")
print(f"F1 (micro): {f1_score(y_true, y_pred, average='micro'):.4f}")
# Informe complet per classe
print("\nInforme detallat per classe:")
print(classification_report(y_true, y_pred, target_names=['Rosa', 'Tulipa', 'Marguerida']))
Matriu de confusió multiclasse:
Predicció
Rosa Tulipa Marguerida
Real Rosa 2 1 0
Tulipa 1 3 0
Marguerida 1 0 2
Cada fila representa els valors reals, cada columna les prediccions. Els elements de la diagonal són els encerts.
Consells per multiclasse:
- Sempre revisa la matriu de confusió → Identifica quines classes es confonen entre elles
- Usa
classification_report→ Mostra precision/recall/F1 per cada classe individualment - Escull l’estratègia d’agregació adequada:
- Classes balancejades i igual importància → macro
- Classes desbalancejades però totes importants → macro
- Vols reflectir distribució real → weighted
- Optimització global → micro
Recomanacions pràctiques
⚠️ Regla d’or: MAI confiïs només en accuracy, especialment amb classes desbalancejades. Un model que sempre prediu la classe majoritària pot tenir 95% accuracy i ser completament inútil.
Workflow recomanat:
- Comença amb la matriu de confusió → Entén quins errors fa el model (FP vs FN)
- Identifica quin error és més costós → Això determina la teva mètrica prioritària
- Avalua amb múltiples mètriques → Cap mètrica única explica tot el comportament
Quina mètrica prioritzar segons el cost dels errors?
| Context | Error més perillós | Mètrica clau | Exemple |
|---|---|---|---|
| Falsos positius costosos | FP | Precision | Spam (no perdre emails importants) |
| Falsos negatius perillosos | FN | Recall | Malalties greus (no deixar casos sense diagnosticar) |
| Ambdós errors importants | FP i FN | F1-Score | Frau (equilibri entre detectar fraus i evitar falses alarmes) |
| Necessites probabilitats | — | Log Loss | Recomanacions (ordenar per confiança) |
| Comparar models | — | ROC-AUC | Avaluació independent del llindar |
Casos especials:
-
Classes desbalancejades (ex: 1% positius)?
- No usar: Accuracy
- Usar: F1-Score, Precision/Recall, ROC-AUC
- Considera: Ajustar pesos, balancejar dades, canviar el llindar
-
Incertesa en els costos dels errors?
- Usa F1-Score com a compromís raonable
- Analitza la corba Precision-Recall per veure el trade-off
- Consulta amb els stakeholders per entendre el negoci
-
Model per a producció amb llindar ajustable?
- Usa ROC-AUC per avaluar capacitat de discriminació
- Ajusta el llindar segons les necessitats del negoci
- Monitoritza Precision i Recall al llindar escollit
Xarxes neuronals
- Origen i justificació
- El perceptró
- Xarxa neuronal bàsica
- Arquitectura d’una xarxa
- Propagació cap endavant
- Entrenament de xarxes neuronals
- Funcions d’activació
- Classificació multiclasse
- Classificació multietiqueta (multilabel)
- Pytorch per tipus de problema
- Optimització avançada
- Grafs computacionals i diferenciació automàtica
- Capes convolucionals
- Càlcul eficient per a xarxes neuronals
Origen i justificació
Les xarxes neuronals són una branca de l’aprenentatge automàtic inspirada en l’estructura i el funcionament del cervell biològic. El seu desenvolupament inicial es va centrar en aplicacions com el reconeixement de la parla, el reconeixement d’imatges i el processament del llenguatge natural (PLN). Amb el temps, aquestes tècniques van evolucionar cap al que avui coneixem com aprenentatge profund (deep learning), que destaca per la utilització de xarxes neuronals amb moltes capes capaces d’aprendre representacions complexes a partir de grans volums de dades.
Amb l’arribada de les xarxes neuronals profundes, l’estructura del pipeline de treball en l’aprenentatge automàtic ha canviat profundament. Mentre que els models tradicionals requereixen una fase prèvia de selecció i disseny manual de característiques (feature engineering), les xarxes neuronals tenen la capacitat d’aprendre representacions útils directament a partir de dades en brut —com imatges, àudio o text sense processar. Això simplifica el desenvolupament de models i permet construir sistemes molt més potents en tasques complexes com la visió per computador, el reconeixement de veu o la traducció automàtica.
Una de les raons fonamentals per les quals aquestes xarxes són tan efectives és la seva capacitat d’escalar amb dades. Escalar, en aquest context, vol dir que el model pot millorar progressivament el seu rendiment a mesura que augmenta la quantitat de dades disponibles. A diferència d’algorismes com la regressió lineal o la regressió logística, que tenen una capacitat d’aprenentatge limitada i sovint no es beneficien de grans volums de dades, les xarxes neuronals poden seguir aprenent patrons més rics i complexos a mesura que se les alimenta amb més informació.
Aquest comportament és possible perquè les xarxes neuronals aprenen representacions internes cada cop més sofisticades, que permeten capturar estructures i relacions profundes dins les dades. Aquestes representacions són la clau per a una millor generalització i per a la creació de models més robustos. La combinació d’aquesta capacitat d’aprenentatge escalable amb l’ús de maquinari especialitzat com les GPU ha estat determinant en l’avenç dels sistemes d’intel·ligència artificial en la darrera dècada.
El perceptró
Abans d’explorar xarxes neuronals complexes, cal entendre l’element bàsic: el perceptró, un model matemàtic inspirat en la neurona biològica proposat per Frank Rosenblatt el 1957.
Funcionament del perceptró
Un perceptró és una unitat computacional simple que imita el comportament d’una neurona: rep múltiples senyals d’entrada, les pondera segons la seva importància, i produeix una sortida binària en funció d’un llindar.

Un perceptró realitza les operacions següents:
- Rep múltiples entrades: x_1, x_2, \ldots, x_n
- Multiplica cada entrada pel seu pes: cada entrada x_i té un pes associat w_i que indica la seva importància
- Suma tots els productes i afegeix un biaix: calcula z = w_1x_1 + w_2x_2 + \cdots + w_nx_n + b
- Aplica una funció d’activació: transforma z en una sortida \hat{y}
Matemàticament, podem expressar-ho com:
z = \mathbf{w} \cdot \mathbf{x} + b = \sum_{i=1}^{n} w_i x_i + b
\hat{y} = g(z)
on g és la funció d’activació. El perceptró original utilitzava una funció esglaó (step function):
g(z) = \begin{cases} 1, & \text{si } z \geq 0 \\ 0, & \text{si } z < 0 \end{cases}
Aquesta funció converteix la combinació lineal ponderada en una decisió binària: activa (1) o inactiva (0).
El perceptró defineix un hiperplà de decisió en l’espai d’entrades. L’equació \mathbf{w} \cdot \mathbf{x} + b = 0 representa aquest hiperplà:
- Els punts on \mathbf{w} \cdot \mathbf{x} + b \geq 0 es classifiquen com a classe 1
- Els punts on \mathbf{w} \cdot \mathbf{x} + b < 0 es classifiquen com a classe 0
En dues dimensions, aquest hiperplà és una línia recta; en tres dimensions, un pla.
Limitació i evolució
El perceptró pot aprendre a separar classes linealment separables, però té una limitació fonamental: no pot resoldre problemes no lineals com el famosa porta lògica XOR.
Aquesta limitació va motivar el desenvolupament de les xarxes neuronals multicapa, que combinen múltiples unitats de processament en capes per aprendre relacions no lineals complexes.
Les xarxes neuronals actuals utilitzen una generalització del perceptró que anomenem neurona artificial (o simplement “neurona”). Vegem les similituds i diferències:
Similituds amb el perceptró:
- Rep múltiples entrades ponderades per pesos w_i
- Suma les entrades amb un biaix b
- Aplica una funció d’activació g(z)
Diferències clau:
- Utilitza funcions d’activació diferenciables (sigmoide, ReLU, tanh) en lloc de la funció esglaó
- Això permet l’entrenament amb retropropagació i descens de gradient, que requereixen derivades
- És un concepte més flexible i general
Terminologia:
- Perceptró: model històric específic amb funció esglaó (Rosenblatt, 1957)
- Neurona artificial: unitat bàsica de les xarxes neuronals modernes, generalització diferenciable del perceptró
D’ara endavant, quan parlem de xarxes neuronals, utilitzarem el terme “neurona” per referir-nos a aquestes unitats modernes que generalitzen el concepte original del perceptró. El perceptró és la pedra angular històrica sobre la qual es construeixen les arquitectures modernes d’aprenentatge profund.
Xarxa neuronal bàsica
Com hem vist, el perceptró clàssic té limitacions importants: només pot aprendre patrons linealment separables. Per superar aquesta limitació, combinem múltiples neurones artificials organitzades en capes, creant així una xarxa neuronal. Aquestes neurones utilitzen funcions d’activació diferenciables (no la funció esglaó del perceptró clàssic), cosa que permet a la xarxa aprendre representacions no lineals complexes mitjançant un procés d’aprenentatge anomenat retropropagació que veurem més endavant.
Vegem un exemple pràctic per entendre com funciona una xarxa neuronal amb capes.
Suposem que volem predir si un producte tindrà una alta demanda. La sortida de la xarxa serà la probabilitat que esdevingui un supervendes.
- Característiques d’entrada: preu, cost d’enviament, esforç de màrqueting, material.
- Capa d’entrada: rep aquests quatre valors sense fer cap càlcul. És una capa estructural.
- Capa oculta: calcula característiques de nivell més alt, també anomenades activacions, aprenent a donar més importància a certes entrades, com per exemple:
- Assequibilitat (a partir de preu i enviament)
- Coneixement del producte (a partir de màrqueting)
- Qualitat percebuda (a partir de preu i material)
- Capa de sortida: rep els valors de la capa oculta i retorna un únic número: la probabilitat estimada.
Les capes situades entre l’entrada i la sortida s’anomenen capes ocultes perquè no interactuen directament amb les dades d’entrada ni amb la sortida final, sinó que aprenen representacions intermèdies útils per resoldre la tasca.
L’arquitectura d’aquesta xarxa és [4-3-1] de dues capes (no comptem la d’entrada) i de tipus feedforward (sense connexions cap enrere).
Tot i que el diagrama només mostra algunes connexions per claredat visual, aquesta és una xarxa totalment connectada o densa (fully connected o dense layer). Això significa que cada neurona de la capa oculta està connectada a totes les 4 entrades, i la neurona de sortida està connectada a totes les 3 neurones de la capa oculta. En total, hi ha:
- 4 × 3 = 12 connexions entre l’entrada i la capa oculta
- 3 × 1 = 3 connexions entre la capa oculta i la sortida
Cada connexió té un pes associat, i cada neurona té un biaix. Per tant, el nombre total de paràmetres entrenables d’aquesta xarxa és:
- Capa oculta: 12 pesos + 3 biaixos = 15 paràmetres
- Capa de sortida: 3 pesos + 1 biaix = 4 paràmetres
- Total: 19 paràmetres
Cadascuna de les característiques de la capa oculta (assequibilitat, coneixement, qualitat) és calculada per una neurona independent. Per exemple, la neurona d’“assequibilitat” rep com a entrades el preu i el cost d’enviament, els multiplica pels seus pesos aprenuts w_1 i w_2, suma el biaix b, i aplica una funció d’activació (com ara sigmoide o ReLU) per produir un valor d’activació. Així, una xarxa neuronal està formada per múltiples neurones treballant en paral·lel dins de cada capa, cadascuna aprenent a detectar patrons específics de les dades.
En el ML tradicional, les característiques rellevants es dissenyen a mà. Però en una xarxa neuronal general, cada neurona d’una capa rep totes les sortides de la capa anterior. Això permet que la xarxa aprengui quines característiques són importants, ajustant els pesos de connexió.
Recordem que cada neurona dins d’una capa utilitza els mateixos elements que hem vist al perceptró:
- Pesos (w): indiquen la importància de cada entrada que rep la neurona.
- Biaix (b): un valor addicional que permet desplaçar l’activació amunt o avall, fins i tot quan totes les entrades són zero. Sense el biaix, la neurona estaria obligada a passar sempre per l’origen i tindria menys flexibilitat per ajustar-se a les dades.
Cada capa transforma les activacions a^{[i]} d’entrada en activacions de sortida mitjançant el mateix procés que al perceptró: combinació lineal ponderada seguida d’una funció d’activació diferenciable.
Aquest procés es repeteix capa rere capa fins a obtenir la sortida final (per exemple, un escalar en regressió):
x \rightarrow a^{[1]} \rightarrow a^{[2]} \rightarrow \dots \rightarrow \hat{y}
Intuïció: en aquest exemple, si ignorem la capa d’entrada, podríem interpretar la xarxa com una regressió logística amb tres entrades, més apropiades que les quatre originals. És com si la xarxa ens fes enginyeria de característiques automàticament.
Arquitectura d’una xarxa
Per construir una xarxa neuronal, cal decidir:
- Quantes capes ocultes tindrà
- Quantes neurones per capa
Aquestes decisions defineixen l’arquitectura de la xarxa. Una arquitectura típica és el perceptró multicapa (Multilayer Perceptron o MLP, un nom històric que s’ha mantingut, malgrat que les seves unitats són neurones modernes amb funcions d’activació diferenciables).
Important sobre el recompte de capes: Quan descrivim una arquitectura, normalment no comptem la capa d’entrada ja que aquesta no fa cap transformació de les dades. Per exemple, una xarxa amb arquitectura [64-25-15-1] té 3 capes entrenables: dues capes ocultes (25 i 15 neurones) i una capa de sortida (1 neurona). La capa d’entrada (64 valors) només passa les dades a la primera capa oculta.
Imaginem una imatge en escala de grisos de 1000×1000 píxels (1 milió de valors). Aplanar la matriu ens dona un vector d’entrada amb 1.000.000 valors.
- Entrada: els valors de brillantor dels píxels (entre 0 i 255)
- Sortida: la probabilitat que la cara sigui de la persona “XYZ”
Què aprèn la xarxa?
- Capa baixa: detecta vores i línies
- Capa intermèdia: reconeix regions facials
- Capa alta: identifica cares completes
Les capes van combinant les representacions detectades segons ens aproximem a la sortida. S’anomenen representacions jeràrquiques, i són descobertes automàticament durant l’entrenament.
Propagació cap endavant
La propagació cap endavant (forward propagation) és el procés pel qual una xarxa neuronal transforma les dades d’entrada en una predicció. És com una cadena de transformacions: cada capa processa la informació que rep de l’anterior i la passa a la següent.
S’utilitza en dues fases:
- A l’entrenament, per calcular la sortida i la seva pèrdua.
- A l’inferència, per calcular la sortida i generar prediccions.
Per exemple, imagina que vols reconèixer si una imatge de 8×8 píxels és un “0” o un “1”:
- Entrada: Els 64 píxels (aplanats en un vector) entren a la xarxa
- Primera capa: Les neurones detecten patrons simples (vores, línies)
- Segona capa: Combina els patrons anteriors en formes més complexes
- Sortida: Una neurona final decideix: “Això és un 1” (o un 0)
Cada capa transforma les dades aplicant pesos, sumant biaixos, i passant el resultat per una funció d’activació (com sigmoide o ReLU).
Per a cada neurona d’una capa:
- Combina les entrades: Multiplica cada valor que rep pel seu pes corresponent i suma-ho tot
- Afegeix el biaix: Suma un valor extra que permet ajustar la sortida
- Aplica l’activació: Transforma el resultat amb una funció no lineal
Matemàticament, per una neurona:
z = w_1 x_1 + w_2 x_2 + \cdots + w_n x_n + b
a = g(z)
On g és la funció d’activació, que introdueix no linealitat al model.
Una de les funcions d’activació més clàssiques és la sigmoide, definida com:
\sigma(z) = \frac{1}{1 + e^{-z}}
Aquestes són les seves característiques principals:
- Sortida entre 0 i 1: Ideal per interpretar com a probabilitats
- Suau i diferenciable: Necessari per poder entrenar la xarxa
- No lineal: Permet a la xarxa aprendre patrons complexos
Per exemple, si z = 0, llavors \sigma(0) = 0.5. Si z és molt positiu (per exemple, z = 5), \sigma(5) \approx 0.993. Si z és molt negatiu (per exemple, z = -5), \sigma(-5) \approx 0.007.
Més endavant veurem altres funcions d’activació com ReLU, però de moment utilitzarem sigmoide per la seva simplicitat i interpretabilitat.
Exemple: Reconèixer dígits 0 vs. 1
Considerem una xarxa amb aquesta arquitectura:
- Entrada: 64 valors (imatge 8×8 aplanada)
- Capa oculta 1: 25 neurones amb activació sigmoide
- Capa oculta 2: 15 neurones amb activació sigmoide
- Sortida: 1 neurona amb sigmoide (probabilitat de ser “1”)
La xarxa processa així, aplicant la funció sigmoide \sigma a cada capa:
a^{(1)} = \sigma(W^{(1)} x + b^{(1)})
a^{(2)} = \sigma(W^{(2)} a^{(1)} + b^{(2)})
\hat{y} = \sigma(W^{(3)} a^{(2)} + b^{(3)})
El valor final \hat{y} (entre 0 i 1) representa la probabilitat que la imatge sigui un “1”. Si \hat{y} \geq 0.5, classifiquem la imatge com a “1”; altrament, com a “0”.
Inferència: fer prediccions
Un cop la xarxa està entrenada, fer una predicció és simplement aplicar la propagació cap endavant. En la pràctica, processem múltiples mostres alhora en lots (batches) per guanyar eficiència:
import torch
# Xarxa ja entrenada
model = ThreeLayerNN()
model.eval() # Mode d'avaluació
# Predicció amb una sola mostra
x = torch.randn(1, 64) # 1 mostra, 64 píxels
with torch.no_grad():
y_pred = model(x)
print(f"Probabilitat: {y_pred.item():.2f}")
# Predicció amb un lot de mostres (més eficient)
X = torch.randn(10, 64) # 10 mostres, 64 píxels cada una
with torch.no_grad():
y_pred_batch = model(X) # 10 prediccions en paral·lel
Internament, PyTorch utilitza operacions matricials que permeten calcular totes les prediccions simultàniament, aprofitant biblioteques optimitzades (BLAS a CPU o cuBLAS a GPU). Això és molt més ràpid que processar cada mostra individualment.
En resum: La propagació cap endavant és una sèrie de transformacions lineals (pesos + biaixos) seguides de funcions no lineals (activacions), capa rere capa, fins a obtenir la predicció final.
Entrenament de xarxes neuronals
L’entrenament d’una xarxa neuronal es fa habitualment en tres passos fonamentals:
- Definició del model
- Definició de la funció de pèrdua i cost
- Entrenament per a minimitzar el cost
Definició del model
Com es calcula la sortida a partir de l’entrada?
Cal especificar com el model calcula la sortida \hat{y} a partir de les dades d’entrada x, utilitzant els paràmetres (pesos i biaixos) que s’entrenaran.
Aquest pas defineix l’arquitectura del model, i pot implementar-se a PyTorch:
import torch
import torch.nn as nn
class ThreeLayerNN(nn.Module):
def __init__(self):
super().__init__()
# Arquitectura: 64 → 25 → 15 → 1
self.hidden1 = nn.Linear(64, 25) # Primera capa oculta
self.hidden2 = nn.Linear(25, 15) # Segona capa oculta
self.output = nn.Linear(15, 1) # Capa de sortida
def forward(self, x):
x = torch.sigmoid(self.hidden1(x)) # Activació capa oculta 1
x = torch.sigmoid(self.hidden2(x)) # Activació capa oculta 2
x = torch.sigmoid(self.output(x)) # Activació sortida (probabilitat)
return x
model = ThreeLayerNN()
El mètode __init__ crea les capes i el mètode forward es crida quan s’utilitza el model com una funció per a calcular les activacions a partir d’un tensor d’entrada.
Aquesta xarxa calcula la sortida de la forma:
\hat{y} = \sigma(W^{(2)} \cdot \sigma(W^{(1)}x + b^{(1)}) + b^{(2)})
on \sigma és la funció sigmoide.
Definició de la funció de pèrdua (loss) i el cost
- Funció de pèrdua: mesura l’error entre la sortida predita \hat{y} i la real y, per una mostra.
- Cost: és la mitjana de la pèrdua sobre totes les mostres del lot o conjunt d’entrenament.
Per a classificació binària, utilitzem Binary Cross-Entropy (BCE):
L(y, \hat{y}) = -[y \log(\hat{y}) + (1-y) \log(1-\hat{y})]
Aquesta funció penalitza més les prediccions molt errònies.
Hi ha dues maneres de calcular aquesta pèrdua en PyTorch, depenent de si el model retorna probabilitats (després del sigmoid) o valors crus (abans del sigmoid, anomenats logits):
# Opció 1: Si el model aplica sigmoid a la sortida
loss_fn = nn.BCELoss()
# Opció 2 (RECOMANADA): Si el model retorna logits (sense sigmoid)
# Aquesta opció és més estable numèricament
loss_fn = nn.BCEWithLogitsLoss()
Què són els logits? Els logits són les sortides crues de la xarxa abans d’aplicar la funció d’activació sigmoid. Utilitzar BCEWithLogitsLoss és més estable numèricament perquè combina el sigmoid i el càlcul de la pèrdua en una sola operació optimitzada, evitant problemes de precisió numèrica.
Entrenament per minimitzar el cost
L’entrenament d’una xarxa neuronal passa per una seqüència de passos que es repeteixen moltes vegades per tal que la xarxa aprengui a fer bones prediccions.
Per a implementar-ho, les llibreries modernes de deep learning utilitzen un mecanisme anomenat diferenciació automàtica que es basa en la construcció d’un graf computacional. Aquest mecanisme s’explica en detall a la secció Grafs computacionals i diferenciació automàtica.
Els passos de l’entrenament són:
-
Passada cap endavant (forward pass)
A partir d’una entrada x, la xarxa calcula una predicció \hat{y}, passant la informació capa per capa fins a la sortida. Cada capa transforma la informació amb pesos, sumes i funcions d’activació.
-
Càlcul de la pèrdua (loss)
Es compara la predicció \hat{y} amb el valor real y. La diferència entre aquests dos valors ens indica com n’ha estat de dolenta (o de bona) la predicció. Aquesta diferència s’expressa amb una funció de pèrdua (com ara l’error quadràtic o la cross-entropy).
-
Passada cap enrere (backward pass o retropropagació)
Aquest pas serveix per aprendre dels errors. Un cop sabem com de malament ho ha fet la xarxa (la pèrdua), volem saber quin pes ha tingut la culpa i com canviar-lo.
La retropropagació calcula, pas a pas i de dreta a esquerra (de la sortida cap a l’entrada), com cada pes ha contribuït a l’error final. Això es fa amb derivades parcials, que ens indiquen si hem de pujar o baixar cada pes per millorar la predicció.
És com si la xarxa es preguntés:
“Si canviés una mica aquest pes… l’error milloraria o empitjoraria?”
Amb això, la xarxa aprèn a corregir-se sola.
-
Actualització dels pesos (optimitzador)
Un algoritme d’optimització (com el descens del gradient o Adam) ajusta els pesos una mica, en la direcció que redueixi l’error. Aquesta actualització es fa segons la fórmula:
w := w - \alpha \frac{\partial L}{\partial w}
On:
- w és un pes de la xarxa
- L és la pèrdua
- \alpha és la taxa d’aprenentatge (learning rate)
La taxa d’aprenentatge controla la mida dels passos durant l’optimització:
- Massa gran → la xarxa no convergeix, oscil·la
- Massa petita → l’entrenament és molt lent
- Valor típic inicial: entre 0.001 i 0.1
Aquest procés es repeteix per cada mostra d’entrenament o per cada mini-batch (petit grup de mostres), i durant moltes èpoques (passades senceres per tot el conjunt d’entrenament).
Durant l’entrenament, és important separar les dades:
- Conjunt d’entrenament: per ajustar els pesos
- Conjunt de validació: per avaluar el rendiment sense sobreajustament
Per a una avaluació més robusta del model, especialment quan el dataset és petit, es pot utilitzar K-Fold Cross-Validation.
Variants de Gradient Descent
Hi ha tres maneres principals d’entrenar una xarxa neuronal, que difereixen en quantes mostres s’utilitzen per calcular el gradient abans d’actualitzar els pesos:
1. Batch Gradient Descent (GD complet)
Utilitza tot el dataset per calcular el gradient i actualitzar els pesos un cop per època.
# Dades d'exemple: X_train shape (num_samples, 64), y_train shape (num_samples, 1)
loss_fn = nn.BCEWithLogitsLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
for epoch in range(1000):
model.train()
logits = model(X_train) # Prediccions: (num_samples, 1)
loss = loss_fn(logits, y_train)
optimizer.zero_grad()
loss.backward() # utilitza els gradients calculats
optimizer.step() # 1 actualització per època
if epoch % 100 == 0:
model.eval()
with torch.no_grad(): # desactiva càlcul de gradients
logits_test = model(X_test)
test_loss = loss_fn(logits_test, y_test)
print(f"Epoch {epoch}: Train Loss = {loss.item():.4f}, Test Loss = {test_loss.item():.4f}")
Avantatges: Gradient precís, convergència estable Inconvenients: Lent, requereix molta memòria, pot quedar atrapat en mínims locals
2. Mini-batch Gradient Descent (recomanat)
Divideix el dataset en grups petits (mini-batches) i actualitza els pesos després de cada grup. És l’enfocament més utilitzat en la pràctica.
En l’entrenament amb mini-batches:
- Una època (epoch) processa tot el conjunt d’entrenament
- Cada època es divideix en mini-batches (grups típicament de 16-512 mostres)
- Els pesos s’actualitzen un cop per mini-batch, després de calcular el gradient promig de les mostres del grup
Per exemple, amb 1000 mostres i batch_size=100:
- Cada època té 10 mini-batches
- Hi ha 10 actualitzacions de pesos per època
- Amb 100 èpoques → 1000 actualitzacions totals
from torch.utils.data import DataLoader, TensorDataset
loss_fn = nn.BCEWithLogitsLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
dataset = TensorDataset(X_train, y_train)
loader = DataLoader(dataset, batch_size=32, shuffle=True)
for epoch in range(1000):
model.train()
for X_batch, y_batch in loader:
logits = model(X_batch)
loss = loss_fn(logits, y_batch)
optimizer.zero_grad()
loss.backward()
optimizer.step() # múltiples actualitzacions per època
En aquest exemple, si el dataset tingués 10 mostres i batch_size=2:
- Cada època processa 10 mostres en 5 mini-batches de 2 mostres
- Hi ha 5 actualitzacions de pesos per època
- Amb 1000 èpoques → 5000 actualitzacions totals
Això coincideix amb l’estructura del codi: el bucle extern (for epoch) es repeteix 1000 vegades, i el bucle intern (for X_batch, y_batch) fa 5 iteracions per època.
Avantatges: Equilibra velocitat (paral·lelització GPU), estabilitat (gradient més suau que mostra individual) i eficiència de memòria (millor que processar tot el dataset alhora) Inconvenients: Requereix ajustar un hiperparàmetre addicional (batch_size)
3. Stochastic Gradient Descent (SGD pur)
Actualitza els pesos després de cada mostra individual (batch_size=1).
from torch.utils.data import DataLoader, TensorDataset
loss_fn = nn.BCEWithLogitsLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
dataset = TensorDataset(X_train, y_train)
loader = DataLoader(dataset, batch_size=1, shuffle=True)
for epoch in range(1000):
model.train()
for X_sample, y_sample in loader:
logits = model(X_sample)
loss = loss_fn(logits, y_sample)
optimizer.zero_grad()
loss.backward()
optimizer.step() # 1 actualització per mostra
Amb 10 mostres i batch_size=1:
- Cada època fa 10 actualitzacions de pesos
- Amb 1000 èpoques → 10000 actualitzacions totals
Avantatges: Actualitzacions freqüents, pot escapar de mínims locals, baixa memòria Inconvenients: Gradient molt sorollós, convergència inestable, difícil aprofitar paral·lelització GPU
Nota: Tot i que l’optimitzador de PyTorch es diu torch.optim.SGD, en realitat és el nom genèric per a gradient descent i es pot usar amb qualsevol dels tres mètodes segons el batch_size que triem.
Comparació de les tres variants:
| Criteri | Batch GD | Mini-batch GD | Stochastic GD |
|---|---|---|---|
| Batch size | Tot el dataset | 16-512 mostres | 1 mostra |
| Actualitzacions/època (1000 mostres) | 1 | ~3-60 | 1000 |
| Qualitat del gradient | Molt precís | Prou precís | Sorollós |
| Velocitat de convergència | Lenta | Ràpida | Variable |
| Ús de memòria | Molt alt | Moderat | Molt baix |
| Paral·lelització GPU | Bona | Excel·lent | Dolenta |
| Capacitat d’escapar de mínims locals | Baixa | Mitjana | Alta |
| Estabilitat | Molt estable | Estable | Inestable |
| Ús pràctic | Poc recomanat | Recomanat | Poc recomanat |
La taula mostra per què mini-batch GD és el mètode més utilitzat: equilibra el nombre d’actualitzacions per època amb l’eficiència computacional i l’estabilitat de l’entrenament.
Modes d’entrenament i avaluació
PyTorch permet canviar el comportament del model segons si estem entrenant o avaluant. Això és important perquè algunes capes especialitzades (com Dropout, que desactiva neurones aleatòriament, o BatchNorm, que normalitza activacions) es comporten de manera diferent en cada mode.
model.train() i model.eval()
PyTorch distingeix entre dos modes d’operació:
model.train(): activa Dropout i BatchNorm, necessari durant l’entrenament.model.eval(): desactiva Dropout i BatchNorm per fer prediccions deterministes.
És una bona pràctica usar sempre aquests modes, fins i tot si el model no té aquestes capes.
# Entrenament
model.train()
for epoch in range(100):
optimizer.zero_grad()
logits = model(X_train)
loss = loss_fn(logits, y_train)
loss.backward()
optimizer.step()
# Avaluació
model.eval()
with torch.no_grad():
logits = model(X_test)
accuracy = (logits.argmax(dim=1) == y_test).float().mean()
torch.no_grad()
Desactiva el seguiment de gradients (el graf computacional) durant l’avaluació (forward pass), estalviant memòria i temps. Per tant, no es podria fer el backward pass (càlcul de gradients). Diferència clau: model.eval() canvia el comportament de capes (Dropout, BatchNorm), mentre que torch.no_grad() només desactiva gradients. Usa’ls junts durant l’avaluació.
Funcions d’activació
Les funcions d’activació determinen com una neurona transforma la seva entrada en una sortida. La tria de la funció adequada és crucial per al rendiment de la xarxa.

Funcions d’activació més comunes
Fins ara hem utilitzat la funció sigmoide a la capa de sortida per a classificació binària, ja que retorna valors entre 0 i 1 interpretables com a probabilitats. Tanmateix, per a les capes ocultes, la sigmoide presenta problemes importants (saturació i gradient que desapareix).
La funció d’activació més utilitzada en capes ocultes és la ReLU (Rectified Linear Unit):
g(z) = \max(0, z)
Aquesta funció retorna 0 si z < 0 , i retorna z si z \geq 0 . És molt més ràpida de calcular que la sigmoide i evita el problema del gradient que desapareix.
Altres funcions d’activació comunes són:
- Funció lineal o identitat: g(z) = z. S’utilitza a la capa de sortida per a regressió, permetent qualsevol valor real.
- Softmax: transforma un vector de valors reals en una distribució de probabilitats que sumen 1. S’utilitza a la capa de sortida per a classificació multiclasse.
Com triar la funció d’activació
Comencem pel cas de la capa de sortida:
Depenent del tipus de variable objectiu y , hi ha una elecció natural per a la funció d’activació:
- 🔹 Classificació binària → usa sigmoide
- 🔹 Regressió amb valors positius i negatius (p.ex. variació del valor d’una acció) → usa funció lineal
- 🔹 Regressió amb només valors positius (p.ex. predicció del preu d’un habitatge) → usa ReLU
- 🔹 Classificació multiclasse → usa softmax
I les capes ocultes?
Per a les capes ocultes, la funció d’activació més habitual és ReLU. Els motius són:
- Més ràpida de calcular que la sigmoide.
- Evita problemes de gradients molt petits: la sigmoide s’aplana tant a l’esquerra com a la dreta del gràfic (quan z \to -\infty o z \to \infty ), fent que la seva derivada sigui gairebé zero. Això provoca que els pesos s’actualitzin molt lentament durant la retropropagació, especialment en xarxes profundes.
- Problema potencial de ReLU: si moltes neurones aprenen pesos que produeixen z < 0 constantment, la seva sortida serà sempre 0 i deixaran d’aprendre (neurones “mortes”). Variants com Leaky ReLU solucionen aquest problema.
Cal evitar usar la funció lineal a les capes ocultes, perquè llavors la nostra xarxa no aprendrà res més enllà d’una regressió lineal.
Per tant, ReLU és la millor opció inicial per a capes ocultes, tot i que hi ha altres alternatives com Leaky ReLU, ELU, o tanh, segons el context del problema.
Exemple de classificació binària amb ReLU
import torch
import torch.nn as nn
import torch.nn.functional as F
class BinaryClassificationNN(nn.Module):
def __init__(self):
super().__init__()
# Arquitectura: 64 → 25 → 15 → 1
self.hidden1 = nn.Linear(64, 25) # Primera capa oculta
self.hidden2 = nn.Linear(25, 15) # Segona capa oculta
self.output = nn.Linear(15, 1) # Capa de sortida
def forward(self, x):
x = F.relu(self.hidden1(x)) # ReLU a capa oculta 1
x = F.relu(self.hidden2(x)) # ReLU a capa oculta 2
x = self.output(x) # Retorna logits (sense sigmoid)
return x
model = BinaryClassificationNN()
loss_fn = nn.BCEWithLogitsLoss() # aplica sigmoid internament
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
# Entrenament (simplificat)
for epoch in range(100):
model.train()
optimizer.zero_grad()
logits = model(X_train)
loss = loss_fn(logits, y_train)
loss.backward()
optimizer.step()
# Avaluació
model.eval()
with torch.no_grad():
logits = model(X_test) # Dades de test
y_prob = torch.sigmoid(logits) # Convertim logits a probabilitats
y_pred = (y_prob > 0.5).float() # Threshold a 0.5
accuracy = (y_pred == y_test).float().mean()
print(f"Accuracy: {accuracy:.2%}")
Exemple de regressió amb ReLU
En problemes de regressió, volem predir un valor continu en lloc d’una classe. Per exemple, predir el preu d’un habitatge a partir de les seves característiques.
Diferències clau respecte a la classificació binària:
- Capa de sortida: no apliquem sigmoid ni cap altra funció d’activació (usem funció lineal/identitat)
- Funció de pèrdua: utilitzem MSELoss (Mean Squared Error) en lloc de BCE
- Mètrica d’avaluació: mesurem l’error mitjà en lloc de l’accuracy
import torch
import torch.nn as nn
import torch.nn.functional as F
class RegressionNN(nn.Module):
def __init__(self):
super().__init__()
# Arquitectura: 10 → 25 → 15 → 1
self.hidden1 = nn.Linear(10, 25) # Primera capa oculta
self.hidden2 = nn.Linear(25, 15) # Segona capa oculta
self.output = nn.Linear(15, 1) # Capa de sortida
def forward(self, x):
x = F.relu(self.hidden1(x)) # ReLU a capa oculta 1
x = F.relu(self.hidden2(x)) # ReLU a capa oculta 2
x = self.output(x) # Sortida lineal (sense activació)
return x
model = RegressionNN()
loss_fn = nn.MSELoss() # Mean Squared Error
optimizer = optim.SGD(model.parameters(), lr=0.1)
# Entrenament (simplificat)
for epoch in range(100):
model.train()
optimizer.zero_grad()
y_pred = model(X_train)
loss = loss_fn(y_pred, y_train)
loss.backward()
optimizer.step()
# Avaluació
model.eval()
with torch.no_grad():
y_pred = model(X_test)
mae = torch.abs(y_pred - y_test).mean()
print(f"MAE: {mae.item():.4f}")
Notes importants sobre regressió:
- Si els valors de sortida són sempre positius (com preus), es pot utilitzar ReLU a la capa de sortida en lloc de la funció lineal
- Si els valors poden ser positius i negatius, cal utilitzar la funció lineal (identitat) a la sortida
- És recomanable normalitzar les dades d’entrada i sortida per millorar la convergència
Resum
| Funció | Rang | Ús típic | Avantatge clau | Desavantatge |
|---|---|---|---|---|
| Sigmoid | (0, 1) | Capa de sortida (classificació binària) | Interpretable com a probabilitat | Saturació → vanishing gradient |
| ReLU | [0, ∞) | Capa oculta | Senzilla i eficient, evita saturació | Pot causar neurones mortes |
| Leaky ReLU | (-∞, ∞) | Capa oculta | Evita neurones mortes | Pot introduir soroll si la pendent negativa és massa alta |
| Tanh | (-1, 1) | Capa oculta | Centrada en zero, útil per dades normalitzades | Saturació → vanishing gradient |
| Lineal (Identitat) | (-∞, ∞) | Capa de sortida (regressió) | Manté escala i signe de la sortida | No introdueix no-linealitat |
| Softmax | (0, 1), ∑ = 1 | Capa de sortida (classificació multi-classe) | Distribució de probabilitats sobre classes | Cost computacional més alt |
Classificació multiclasse
La regressió logística és una tècnica adequada per a problemes de classificació binària, és a dir, quan només hi ha dues classes possibles.
Quan treballem amb múltiples classes, necessitem aprendre una frontera de decisió que separi totes les classes. En aquest cas, utilitzem la regressió softmax, una generalització de la regressió logística que permet afrontar problemes de classificació multiclasse.
En classificació binària tenim 1 sortida: “probabilitat que sigui de classe 1”.
En classificació multiclasse necessitem N sortides (una per cada classe). Per exemple, per reconèixer dígits (0-9) necessitem 10 sortides.
El problema: volem que aquestes 10 sortides representin probabilitats que sumin 1.
Exemple concret:
- Entrada: imatge d’un “3” escrit a mà
- Logits (sortides abans de softmax): [0.1, -0.5, 0.3, 4.2, 0.8, -1.0, 0.2, -0.3, 0.5, 0.1]
- Després de softmax: [0.02, 0.01, 0.03, 0.85, 0.04, 0.01, 0.02, 0.01, 0.03, 0.02]
- Interpretació: 85% de confiança que és un “3”, 4% que és un “4”, etc.
Què fa softmax?
- Converteix valors arbitraris (logits) en probabilitats (entre 0 i 1)
- Garanteix que totes les probabilitats sumin exactament 1
- Els valors més alts (logits) es converteixen en probabilitats més altes
Fonaments matemàtics
Suposem que la variable objectiu y pot prendre els valors 1, 2, 3, \ldots, N. Definim:
z_j = \hat{\mathbf{w}}_j \cdot \hat{\mathbf{x}} + b_j, \quad j = 1, \ldots, N
on:
- \hat{\mathbf{x}} és el vector d’entrada (amb el biaix inclòs si cal),
- \hat{\mathbf{w}}_j són els pesos associats a la classe j,
- b_j és el biaix corresponent a la classe j.
La funció d’activació softmax per a la classe j es defineix com:
a_j = \frac{e^{z_j}}{\sum\limits_{k=1}^{N} e^{z_k}} = P(y = j ,|, \hat{\mathbf{x}})
Aquesta expressió garanteix que:
a_1 + a_2 + \ldots + a_N = 1
Per tant, cada a_j es pot interpretar com la probabilitat que la mostra pertanyi a la classe j.
Funció de pèrdua
La funció de pèrdua utilitzada és l’entropia creuada:
\text{loss} = -\log(a_j) \quad \text{si } y = j
És a dir, només es penalitza la probabilitat assignada a la classe correcta.
Una capa softmax és especial perquè el càlcul de les seves activacions depèn de tots els valors z_j simultàniament. En classificació binària, cada neurona tenia una sortida independent. En canvi, en classificació multiclasse, el càlcul de cada a_j es fa en relació amb totes les altres sortides. Aquesta normalització fa que la sortida sigui una distribució de probabilitat sobre les classes possibles.
Consideracions en PyTorch
En problemes de classificació multiclasse amb PyTorch, és habitual utilitzar nn.CrossEntropyLoss. Cal tenir en compte:
- El model ha de retornar els logits, és a dir, els valors z abans d’aplicar softmax.
Els logits són les puntuacions no normalitzades que surten de l’última capa (
Linear) del model. - No s’ha d’aplicar
softmaxdins del mètodeforward, ja queCrossEntropyLossja l’aplica de forma interna, eficient i estable.
Per què és millor no aplicar softmax manualment?
Calcular softmax i després log pot generar problemes de precisió numèrica, sobretot quan les probabilitats són molt petites. PyTorch optimitza aquest procés per evitar aquest tipus d’errors.
Exemple: dígits manuscrits
Per exemple, en el cas de la classificació de dígits manuscrits (de 0 a 9), treballem amb 10 classes.
L’última capa del nostre model deixarà de tenir una sola unitat (com en la classificació binària) i en tindrà 10, una per cada dígit. Aquesta última capa seria una capa softmax.
# Definir el model
class MulticlassNN(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(64, 25)
self.fc2 = nn.Linear(25, 15)
self.output = nn.Linear(15, 10) # 10 classes
def forward(self, x):
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.output(x) # No softmax here!
return x
model = MulticlassNN()
loss_fn = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.1)
# Entrenament (simplificat)
for epoch in range(100):
model.train()
optimizer.zero_grad()
logits = model(X_train)
loss = loss_fn(logits, y_train)
loss.backward()
optimizer.step()
# Avaluació
model.eval()
with torch.no_grad():
logits = model(X_test)
y_pred = torch.argmax(logits, dim=1)
accuracy = (y_pred == y_test).float().mean()
print(f"Accuracy: {accuracy:.2%}")
Classificació multietiqueta (multilabel)
En aquest tipus de problemes, una sola entrada pot estar associada a diverses etiquetes binàries. Per tant, la variable objectiu \mathbf{y} és un vector de N possibles etiquetes, cadascun dels quals pot ser 0 o 1.
Una estratègia possible per abordar aquest problema seria construir N xarxes neuronals independents, una per a cada etiqueta. No obstant això, hi ha una alternativa més eficient: entrenar una sola xarxa amb N sortides.
En aquest cas:
- L’última capa tindrà N unitats de sortida.
- Cada unitat aplica una funció d’activació sigmoide.
- Això permet que cada sortida \hat{y}_j representi la probabilitat que l’etiqueta j estigui present (valgui 1), de manera independent de les altres.
Aquest enfocament aprofita el fet que, tot i que les etiquetes són múltiples, no són mútuament excloents, i per tant no cal una normalització com la que fa softmax.
class MultiLabelNN(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(100, 64)
self.fc2 = nn.Linear(64, 32)
self.output = nn.Linear(32, 5) # 5 sortides, 1 per etiqueta
def forward(self, x):
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.output(x) # Logits (NO sigmoid aquí)
return x
# Exemple: 5 etiquetes (Acció, Sci-fi, Drama, Comèdia, Terror)
model = MultiLabelNN()
loss_fn = nn.BCEWithLogitsLoss() # (sigmoid + BCE combinats)
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
# Entrenament (simplificat)
for epoch in range(100):
model.train()
optimizer.zero_grad()
logits = model(X)
loss = loss_fn(logits, y)
loss.backward()
optimizer.step()
# Avaluació
model.eval()
with torch.no_grad():
logits = model(X[:5])
y_prob = torch.sigmoid(logits)
y_pred = (y_prob > 0.5).float()
print(f"Predicció: {y_pred[0]}")
print(f"Real: {y[0]}")
Pytorch per tipus de problema
A continuació es mostren cadascun dels tipus de problemes vistos i els aspectes específics que cal considerar.
| Aspecte | Classificació Binària | Classificació Multiclasse | Classificació Multilabel | Regressió |
|---|---|---|---|---|
| Capa de Sortida | 1 neurona | C neurones | C neurones | 1 neurona (o més) |
| Activació a la Sortida | Cap (logits) | Cap (logits) | Cap (logits) | Cap (lineal) |
| Funció de Pèrdua | nn.BCEWithLogitsLoss() | nn.CrossEntropyLoss() | nn.BCEWithLogitsLoss() | nn.MSELoss() o nn.L1Loss() |
| Format Targets | [batch, 1] float (0.0 o 1.0) | [batch] long (índexs) | [batch, C] float (0s i 1s) | [batch, 1] float (continus) |
| Com fer Prediccions | torch.sigmoid(output) > 0.5 | torch.argmax(output, dim=1) | torch.sigmoid(output) > 0.5 | output directament |
| Com obtenir Probabilitats | torch.sigmoid(output) | F.softmax(output, dim=1) | torch.sigmoid(output) | N/A |
| Exemple Target | [1.0] (classe positiva) | 2 (classe índex 2) | [0, 1, 1, 0] (labels 1 i 2) | [23.5] (valor continu) |
Optimització avançada
El descens de gradient és un mètode per trobar el mínim de la funció de cost fent passos de mida \alpha (la taxa d’aprenentatge). L’actualització d’un pes w_j en un sol pas es formula com:
w_j := w_j - \alpha \frac{\partial J}{\partial w_j}
On:
- \alpha (learning rate) controla la mida del pas.
- \frac{\partial J}{\partial w_j} és la derivada de la funció de cost respecte al pes w_j.
Si tots els passos van en la mateixa direcció, podríem augmentar \alpha per accelerar la convergència. Tanmateix, un pas massa gran pot provocar oscil·lacions o divergència.
Adam (Adaptive Moment Estimation) és un algorisme que ajusta automàticament la taxa d’aprenentatge per a cada paràmetre (pesos i biaixos). Això permet:
- Augmentar la taxa d’aprenentatge si els passos són petits i consistents.
- Reduir la taxa d’aprenentatge si hi ha oscil·lacions per salts massa grans.
Per canviar l’optimitzador a Adam utilitzant PyTorch, només cal substituir optim.SGD per optim.Adam:
# Abans (SGD):
optimizer = optim.SGD(model.parameters(), lr=0.1)
# Després (Adam):
# Adam necessita una learning rate més petita que SGD (típicament 1e-3 o 1e-4)
optimizer = optim.Adam(model.parameters(), lr=1e-3)
Important: Adam sol necessitar una taxa d’aprenentatge molt més petita que SGD. Si utilitzes lr=0.1 amb Adam, l’entrenament probablement divergirà. Els valors típics per Adam són 1e-3 (0.001) o 1e-4 (0.0001).
Adam és una excel·lent elecció per defecte a l’hora d’optimitzar xarxes neuronals gràcies a la seva capacitat d’adaptar la taxa d’aprenentatge durant l’entrenament.
Grafs computacionals i diferenciació automàtica
Les llibreries modernes de deep learning (com PyTorch o TensorFlow) utilitzen un mecanisme anomenat diferenciació automàtica (automatic differentiation o autograd). Aquest mecanisme es basa en la construcció d’un graf computacional.
Què és un graf computacional?
Un graf computacional és una representació interna que la llibreria crea automàticament mentre es fan càlculs. Està format per:
- Nodes: representen les operacions matemàtiques (multiplicacions, sumes, funcions d’activació)
- Arestes: connecten les operacions seguint l’ordre en què s’executen
Aquest graf registra totes les operacions que es fan sobre els tensors durant la passada cap endavant, com ara:
- Transformacions lineals (multiplicacions de matrius i sumes)
- Funcions d’activació (ReLU, sigmoid, tanh, etc.)
- Funcions de pèrdua (cross-entropy, MSE, etc.)
El més important és que també emmagatzema com calcular les derivades de cada operació.
Per què serveix el graf computacional?
El graf s’utilitza en dos moments diferents de l’entrenament:
-
Passada endavant (forward pass): mentre calculem la predicció, el graf es va construint automàticament, registrant cada operació.
-
Passada enrere (backward pass): quan volem aprendre dels errors, el graf es recorre en sentit invers per calcular les derivades de forma automàtica, sense que nosaltres les hàgim de programar manualment.
Aquest procés automàtic de calcular derivades s’anomena diferenciació automàtica, i és el que fa possible entrenar xarxes neuronals amb milions de paràmetres sense haver de calcular derivades a mà.
Com funciona la diferenciació automàtica?
Durant l’entrenament, els tensors (les estructures de dades que contenen els pesos i les dades) tenen la capacitat de “recordar” totes les operacions que s’han fet sobre ells. Quan arribem al final i tenim la pèrdua calculada, podem demanar a la llibreria que calculi automàticament les derivades de la pèrdua respecte a cada pes de la xarxa.
Això ho fa aplicant la regla de la cadena del càlcul diferencial de forma automàtica: va descomponent les derivades complexes en derivades simples d’operacions bàsiques (suma, multiplicació, funcions d’activació), i les combina per obtenir el gradient de cada paràmetre.
En resum: els tensors diferencien totes les funcions que s’han executat durant la passada cap endavant (forward pass), però ho fan durant la passada cap enrere (backward pass), recorrent el graf computacional en sentit invers.
El resultat: sabem exactament quant hem d’ajustar cada pes per reduir l’error, sense haver de derivar ni una sola equació manualment.
La clau de l’entrenament modern de xarxes neuronals és aquesta diferenciació automàtica: el càlcul automàtic de derivades mitjançant el graf computacional. Això permet entrenar models amb milions de paràmetres sense haver d’escriure ni una sola derivada a mà.
Capes convolucionals
Fins ara hem treballat amb capes totalment connectades (dense layers), on cada neurona està connectada a totes les sortides de la capa anterior. Aquest enfocament funciona bé per a moltes tasques, però quan treballem amb dades estructurades com imatges o senyals temporals, hi ha un tipus de capa més eficient: les capes convolucionals.
Una capa convolucional és una capa especial on cada neurona només “veu” una regió local de l’entrada procedent de la capa anterior. Això significa que, en lloc d’estar connectada a totes les sortides anteriors, cada neurona aplica un petit filtre (o kernel) a una finestra reduïda de valors.
Avantatges:
-
Càlcul més ràpid
Com que cada neurona processa només una part de l’entrada, es redueix el nombre de connexions i, per tant, la quantitat de càlcul necessari. -
Menys dades d’entrenament i menor sobreajustament
Els filtres locals comparteixen pesos, de manera que el model té menys paràmetres i resulta menys procliu a memoritzar dades específiques.
Quan encadenem diverses capes convolucionals en una xarxa, obtenim una xarxa neuronal convolucional (CNN), que és especialment eficaç per al processament d’imatges i sèries temporals.
Exemple pràctic (1D): suposem que tenim un senyal d’ECG d’1 dimensió amb 100 punts d’entrada: x_1, x_2, \dots, x_{100}.
-
Primera capa oculta
Utilitza un filtre de grandària 20 i pas (stride) de 10 per generar 9 activacions. Cada activació processa finestres des de x_1\dots x_{20}, x_{11}\dots x_{30}, x_{21}\dots x_{40}, etc. -
Segona capa oculta
Aplica un filtre de grandària 5 i pas de 2 sobre les 9 activacions anteriors per obtenir 3 noves activacions, que corresponen a les finestres a_1\dots a_5, a_3\dots a_7 i a_5\dots a_9. -
Capa de sortida
Acaba amb una funció sigmoide que retorna la probabilitat de presència o absència de malaltia cardíaca.
Aquest procés permet que la xarxa detecti patrons locals (per exemple, formes d’ones característiques de l’ECG) i els combini progressivament per fer una classificació robusta.
Càlcul eficient per a xarxes neuronals
Quan treballem amb xarxes neuronals, el càlcul de la inferència pot ser molt costós si es fa mostra per mostra i neurona per neurona. Per això utilitzem biblioteques optimitzades que aprofiten el maquinari modern.
A continuació t’expliquem les principals tecnologies implicades.
BLAS (Basic Linear Algebra Subprograms)
És una especificació estàndard de rutines bàsiques d’àlgebra lineal (com multiplicacions de matrius i vectors).
- Funciona en CPU
- Les seves implementacions (com OpenBLAS, MKL, ATLAS…) són molt optimitzades
- És la base de moltes biblioteques científiques (
NumPy,SciPy, etc.)
Exemple: multiplicar dues matrius grans amb numpy.dot() utilitza BLAS per fer-ho ràpidament.
GPU (Graphics Processing Unit)
Una GPU és un processador paral·lel que pot executar milers de càlculs al mateix temps. Tot i que es va dissenyar per a gràfics, avui s’utilitza àmpliament per a càlcul científic i d’IA.
- Té milers de nuclis simples (en comparació amb pocs nuclis potents d’una CPU)
- Ideal per a càlculs repetitius i paral·lels com els de l’àlgebra lineal
CUDA (Compute Unified Device Architecture)
És la plataforma de programació creada per NVIDIA per desenvolupar aplicacions que s’executin a les seves GPU.
- Permet escriure codi que s’executa directament dins la GPU
- Exposa una API per controlar memòria, fils d’execució, etc.
- Moltes biblioteques populars (com PyTorch o TensorFlow) fan servir CUDA per sota si hi ha GPU disponible
cuBLAS (CUDA BLAS)
És la versió de BLAS implementada per NVIDIA per executar-se sobre GPU amb CUDA.
- Té les mateixes operacions que BLAS, però molt més ràpides a la GPU
- L’ús de
torch.matmulotf.matmulen dispositius CUDA utilitza cuBLAS per sota - Aprofita tot el paral·lelisme de la GPU per multiplicar matrius de forma massiva
Resum:
| Tecnologia | Funciona a… | Fa què? | Exemples d’ús |
|---|---|---|---|
| BLAS | CPU | Operacions bàsiques d’àlgebra lineal | numpy.dot, scipy.linalg |
| GPU | Targeta gràfica | Execució massiva de càlculs paral·lels | torch.Tensor.to("cuda") |
| CUDA | GPU NVIDIA | Plataforma per programar GPUs | PyTorch, TensorFlow, numba |
| cuBLAS | GPU NVIDIA | Versió GPU de BLAS | torch.matmul amb CUDA activat |
Aquestes tecnologies són claus per fer que les xarxes neuronals siguin ràpides i escalables. Quan entrenes o fas inferència amb models grans, estar familiaritzat amb aquestes eines t’ajuda a entendre què passa “sota el capó”.
Organització de matrius i dimensions
Per aprofitar aquestes tecnologies d’acceleració, cal entendre com s’organitzen les dades en format matricial.
Dimensions típiques en processament per lots
Suposem que tenim:
- m = 3 mostres d’entrada al lot (batch)
- Cada mostra és un vector d’entrada amb n_{\text{in}} = 4 característiques
- La capa té n_{\text{out}} = 2 neurones
Matriu d’entrada X
Conté les mostres del lot:
X = \begin{bmatrix} x_{1}^{(1)} & x_{1}^{(2)} & x_{1}^{(3)} \\ x_{2}^{(1)} & x_{2}^{(2)} & x_{2}^{(3)} \\ x_{3}^{(1)} & x_{3}^{(2)} & x_{3}^{(3)} \\ x_{4}^{(1)} & x_{4}^{(2)} & x_{4}^{(3)} \\ \end{bmatrix} \in \mathbb{R}^{4 \times 3}
Cada columna és una mostra amb les seves característiques.
Matriu de pesos W
Cada fila de W conté els pesos d’una neurona:
W = \begin{bmatrix} w_{1,1} & w_{1,2} & w_{1,3} & w_{1,4} \\ w_{2,1} & w_{2,2} & w_{2,3} & w_{2,4} \\ \end{bmatrix} \in \mathbb{R}^{2 \times 4}
Vector de biaixos b
b = \begin{bmatrix} b_1 \ b_2 \end{bmatrix} \in \mathbb{R}^{2}
Càlcul de l’activació lineal Z
Fem la multiplicació i sumem el biaix:
Z = W \cdot X + b = \begin{bmatrix} z_1^{(1)} & z_1^{(2)} & z_1^{(3)} \\ z_2^{(1)} & z_2^{(2)} & z_2^{(3)} \\ \end{bmatrix} \in \mathbb{R}^{2 \times 3}
Cada columna de Z és la sortida (abans de l’activació) per una mostra.
Resum de dimensions
| Matriu | Dimensions | Significat |
|---|---|---|
| X | n_{\text{in}} \times m | Entrades: cada columna és una mostra |
| W | n_{\text{out}} \times n_{\text{in}} | Pesos: cada fila correspon a una neurona |
| b | n_{\text{out}} | Biaixos: un valor per neurona |
| Z | n_{\text{out}} \times m | Activacions lineals: cada columna és l’output per mostra |
El producte W \cdot X és possible perquè les dimensions coincideixen: (n_{\text{out}} \times n_{\text{in}}) \cdot (n_{\text{in}} \times m) = (n_{\text{out}} \times m).
Transposada d’una matriu
La transposada d’una matriu A, notada A^T, s’obté intercanviant files per columnes:
A = \begin{bmatrix} a_{11} & a_{12} & a_{13} \\ a_{21} & a_{22} & a_{23} \end{bmatrix} \quad\Rightarrow\quad A^T = \begin{bmatrix} a_{11} & a_{21} \\ a_{12} & a_{22} \\ a_{13} & a_{23} \end{bmatrix}
Si A és m \times n, llavors A^T és n \times m.
Ús pràctic: diferents convencions
Algunes biblioteques (com NumPy) representen X amb forma m \times n_{\text{in}}, és a dir, cada fila és una mostra. En aquest cas, cal fer servir la transposada dels pesos:
Z = X \cdot W^T + b
On:
- X té forma m \times n_{\text{in}}
- W^T té forma n_{\text{in}} \times n_{\text{out}}
- Z té forma m \times n_{\text{out}}
Exemple en PyTorch:
import torch
m = 10 # nombre de mostres
n_in = 64 # característiques d'entrada
n_out = 25 # neurones a la capa
# Dades d'entrada (cada fila és una mostra)
X = torch.randn(m, n_in) # (10, 64)
# Pesos i biaixos
W = torch.randn(n_out, n_in) # (25, 64)
b = torch.randn(n_out) # (25,)
# Càlcul vectoritzat per tot el lot
Z = torch.matmul(X, W.T) + b # (10, 25)
# Equivalent: Z = X @ W.T + b
print("Forma de Z:", Z.shape) # torch.Size([10, 25])
Aquest enfocament vectoritzat permet processar moltes mostres simultàniament, aprofitant al màxim les capacitats de BLAS (a CPU) o cuBLAS (a GPU).
Guia d’aplicació i diagnòstic
- Què podem fer
- Avaluació d’un model
- Selecció de model
- Biaix i variància
- Regularització
- Rendiment de referència
- Curves d’aprenentatge
- Què podem fer (revisat)
- Biaix i variància en xarxes neuronals
- Altres tècniques
Aquest document se centra en tècniques de diagnòstic i anàlisi matemàtica per entendre i millorar models de Machine Learning. Proporciona:
- Formules matemàtiques per calcular errors i mètriques
- Visualitzacions interactives per comprendre conceptes clau
- Procediments detallats amb exemples de codi
- Eines de diagnòstic basades en anàlisi quantitativa
Complement: Per una guia pràctica sobre quin algoritme triar i com estructurar projectes de ML, consulta la Metodologia Pràctica.
Aquest document presenta un marc sistemàtic de diagnòstic amb bases matemàtiques per identificar i resoldre problemes en models de Machine Learning.
Què podem fer
Què fer si J(\vec{w}, b) d’una regressió lineal regularitzada dona errors massa grans en les prediccions?
Si el cost o error de la funció de pèrdua és massa alt, pots provar les següents opcions:
- Obtenir més exemples d’entrenament.
- Reduir el conjunt de característiques (features).
- Afegir noves característiques.
- Afegir característiques polinòmiques (per exemple, x_1^2, x_2^2, \ldots ).
- Disminuir el paràmetre de regularització \lambda.
- Incrementar el paràmetre de regularització \lambda.
La necessitat de diagnòstics: cal fer proves i anàlisis que ens ajudin a entendre què està funcionant i què no en un algorisme d’aprenentatge, per així guiar les millores en el seu rendiment.
Avaluació d’un model
Suposem que tenim un model amb quatre característiques i polinòmiques, i que la seva corba d’ajust és molt irregular (“wiggly”). Probablement és un mal model que no predirà bé.
Preguntes:
- Com podem visualitzar un model amb quatre dimensions? És difícil de representar.
Una pràctica habitual és dividir el conjunt de dades en:
- Conjunt d’entrenament (train): 70% de les dades.
- Conjunt de prova (test): 30% de les dades.
Primer entrenem el model amb el conjunt d’entrenament, i després avaluem el seu rendiment al conjunt de prova.
- m_{train} : nombre d’exemples d’entrenament
- m_{test} : nombre d’exemples de prova
A continuació es mostra el procediment d’entrenament i test per a una regressió lineal amb funció de cost de mínims quadrats.
-
Ajustament dels paràmetres (inclou regularització):
J(\overrightarrow{w}, b) = \frac{1}{2 m_{\text{train}}} \sum_{i=1}^{m_{\text{train}}} \left( f_{\overrightarrow{w}, b}(\overrightarrow{x}^{(i)}) - y^{(i)} \right)^2 + \frac{\lambda}{2 m_{\text{train}}} \sum_{j=1}^n w_j^2
-
Càlcul de l’error al conjunt de prova:
J_{\text{test}}(\overrightarrow{w}, b) = \frac{1}{2 m_{\text{test}}} \sum_{i=1}^{m_{\text{test}}} \left( f_{\overrightarrow{w}, b}(\overrightarrow{x}_{\text{test}}^{(i)}) - y_{\text{test}}^{(i)} \right)^2
-
Càlcul de l’error al conjunt d’entrenament:
J_{\text{train}}(\overrightarrow{w}, b) = \frac{1}{2 m_{\text{train}}} \sum_{i=1}^{m_{\text{train}}} \left( f_{\overrightarrow{w}, b}(\overrightarrow{x}_{\text{train}}^{(i)}) - y_{\text{train}}^{(i)} \right)^2
Què veurem a les funcions de cost si tenim overfitting (sobreajustament)?
- J_{train} és baix (el model s’ajusta molt bé a l’entrenament).
- J_{test} és alt (el model no generalitza bé a noves dades).
En problemes de classificació, els valors J_{train} i J_{test} es refereixen a la fracció d’exemples mal classificats al conjunt d’entrenament i al conjunt de prova, respectivament.
Selecció de model
Podem ajustar models amb funcions de diferents graus ( d = 1, 2, 3, \ldots ), per exemple polinomis:
\begin{aligned} f_{w,b}(x) &= w_1 x + b \\ f_{w,b}(x) &= w_1 x + w_2 x^2 + b \\ f_{w,b}(x) &= w_1 x + w_2 x^2 + w_3 x^3 + b \\ &\quad \vdots \end{aligned}
Una manera intuïtiva és provar quin grau d ens dóna el valor més baix de la funció de pèrdua sobre el conjunt de test, J_{\text{test}} . Per exemple, podríem trobar que d = 5 sembla el millor.
Problema d’usar el conjunt de test per a seleccionar el model: el valor J_{\text{test}} pot ser una estimació massa optimista de l’error real de generalització. Això passa perquè estem triant el grau del polinomi basant-nos en el mateix conjunt de test. En altres paraules:
- Els paràmetres w, b estan ajustats al conjunt d’entrenament.
- El grau d es tria mirant el conjunt de test.
Aquesta pràctica no és correcta, perquè estem utilitzant informació del test per prendre decisions de model, el que fa que la estimació final del error sigui massa optimista i poc fiable.
Solució: dividir les dades en tres conjunts
La manera correcta és dividir les dades en tres parts:
- Conjunt d’entrenament (train): 60% de les dades.
- Conjunt de validació (cross-validation):: 20% de les dades.
- Conjunt de prova (test): 20% de les dades.
El conjunt de validació (també anomenat validation set o dev set) s’utilitza per seleccionar l’hiperparàmetre, en aquest cas, el grau del polinomi d .
Procediment amb els tres conjunts:
- Per cada grau d , entrenem el model amb el conjunt d’entrenament.
- Calculem l’error de validació J_{\text{cv}} en el conjunt de validació.
- Triem el grau d que dóna el menor error J_{\text{cv}} . Per exemple, d = 4 .
- Finalment, utilitzem el conjunt de test per estimar de manera justa l’error de generalització J_{\text{test}} del model final (amb d=4 ).
Aquest procediment no només s’aplica a la selecció del grau del polinomi, sinó també a qualsevol decisió sobre l’arquitectura o paràmetres d’un model, com per exemple el nombre de capes o neurones d’una xarxa neuronal.
Regla d’or: No utilitzar mai el conjunt de test per prendre decisions sobre el model. Només quan ja es tingui el model final, es pot avaluar amb el test per obtenir una estimació realista de la seva qualitat.
K-Fold Cross-Validation
El mètode descrit anteriorment utilitza una única divisió de les dades en train/validació/test. Però, què passa si la divisió és poc afortunada? Per exemple, si per casualitat el conjunt de validació conté exemples més fàcils o més difícils que la mitjana, l’estimació de ( J_{\text{cv}} ) pot ser enganyosa.
La validació creuada de K folds (K-Fold Cross-Validation) és una tècnica més robusta que soluciona aquest problema fent múltiples divisions i calculant la mitjana dels resultats.
Com funciona K-Fold Cross-Validation:
- Dividim les dades en K parts iguals (folds). Per exemple, si ( K = 5 ), dividim les dades en 5 grups.
- Entrenem K vegades, cada vegada utilitzant un fold diferent com a validació i els altres ( K-1 ) folds com a entrenament.
- Calculem la mitjana de l’error de validació obtingut en cada iteració.
Exemple pràctic amb K=5:
- Fold 1: Entrenem amb folds 2-3-4-5, validem amb fold 1 → J_{\text{cv}}^{(1)}
- Fold 2: Entrenem amb folds 1-3-4-5, validem amb fold 2 → J_{\text{cv}}^{(2)}
- Fold 3: Entrenem amb folds 1-2-4-5, validem amb fold 3 → J_{\text{cv}}^{(3)}
- Fold 4: Entrenem amb folds 1-2-3-5, validem amb fold 4 → J_{\text{cv}}^{(4)}
- Fold 5: Entrenem amb folds 1-2-3-4, validem amb fold 5 → J_{\text{cv}}^{(5)}
Error final de validació:
J_{\text{cv}} = \frac{1}{K} \sum_{i=1}^{K} J_{\text{cv}}^{(i)}
Avantatges de K-Fold Cross-Validation:
- Més robust: Redueix la variància de l’estimació de l’error perquè no depèn d’una sola divisió.
- Millor aprofitament de les dades: Cada exemple s’utilitza tant per entrenar com per validar.
- Estimació més fiable: Especialment important amb datasets petits.
Inconvenients:
- Cost computacional: Cal entrenar el model K vegades (en lloc d’una sola vegada).
- Més lent: Pot ser prohibitiu amb models grans o datasets enormes.
Quan usar K-Fold Cross-Validation:
- Datasets petits o mitjans (menys de 100k exemples aproximadament).
- Quan vols una estimació fiable de l’error de generalització.
- Per ajustar hiperparàmetres (per exemple, triar ( \lambda ), el grau del polinomi, etc.).
- Quan el temps d’entrenament no és un problema crític.
Quan NO usar-lo:
- Datasets molt grans (milions d’exemples): massa costós computacionalment.
- Models molt lents d’entrenar (xarxes neuronals profundes): millor usar una única divisió train/val/test.
Variants comunes:
- K=5 o K=10: Valors típics que equilibren cost computacional i robustesa.
- Leave-One-Out (LOO): Cas extrem on ( K = m ) (nombre total de mostres). Cada exemple és un fold. Molt costós però màxima robustesa.
- Stratified K-Fold: En problemes de classificació amb classes desbalancejades, garanteix que cada fold mantingui la mateixa proporció de classes que el dataset original.
Exemple amb Linear Regression:
import numpy as np
from sklearn.model_selection import cross_val_score, KFold
from sklearn.linear_model import LinearRegression
# Generar dades sintètiques per a regressió
rng = np.random.RandomState(42)
X_train = rng.randn(100, 5)
y_train = rng.randn(100)
# Crear el model
model = LinearRegression()
# Configurar K-Fold amb K=5
cv = KFold(n_splits=5, shuffle=True, random_state=42)
# Avaluar el model amb cross-validation
cv_scores = cross_val_score(
estimator=model,
X=X_train,
y=y_train,
cv=cv,
scoring='neg_mean_squared_error',
n_jobs=-1 # Usar tots els cores disponibles
)
# Convertir a MSE positiu
mse_scores = -cv_scores
print(f"MSE per cada fold: {mse_scores}")
print(f"MSE mitjà: {mse_scores.mean():.4f} ± {mse_scores.std():.4f}")
Alternativament, pots utilitzar Stratified K-Fold per a classificació:
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
Exemple amb xarxes neuronals:
import torch
import torch.nn as nn
from torch.utils.data import DataLoader, Subset
from sklearn.model_selection import KFold
import numpy as np
# Configuració
k = 5 # Nombre de folds
seed = 42
kf = KFold(n_splits=k, shuffle=True, random_state=seed)
fold_accs = []
for fold, (train_idx, val_idx) in enumerate(kf.split(X), 1):
# Seed diferent per cada fold per evitar inicialitzacions idèntiques
torch.manual_seed(seed + fold)
# DataLoaders: batch més gran per validació (no cal backprop)
train_loader = DataLoader(Subset(dataset, train_idx), batch_size=batch_size, shuffle=True)
val_loader = DataLoader(Subset(dataset, val_idx), batch_size=batch_size*2, shuffle=False)
# Model, optimitzador i loss
model = MLP(in_features, num_classes=num_classes)
optimizer = torch.optim.Adam(model.parameters(), lr=lr)
criterion = nn.CrossEntropyLoss()
# Entrenament
for epoch in range(epochs):
model.train()
for xb, yb in train_loader:
optimizer.zero_grad()
loss = criterion(model(xb), yb)
loss.backward()
optimizer.step()
# Avaluació
model.eval()
correct = total = 0
with torch.no_grad():
for xb, yb in val_loader:
preds = model(xb).argmax(dim=1)
correct += (preds == yb).sum().item()
total += yb.size(0)
acc = correct / total
fold_accs.append(acc)
print(f"Fold {fold}/{k}: val_acc={acc:.4f}")
print(f"\nMean acc: {np.mean(fold_accs):.4f} ± {np.std(fold_accs):.4f}")
Resum del workflow recomanat:
- Dividir les dades inicialment en train+val (80%) i test (20%).
- Aplicar K-Fold Cross-Validation sobre train+val per:
- Seleccionar hiperparàmetres
- Comparar models
- Obtenir una estimació robusta de l’error
- Entrenar el model final amb tot el conjunt train+val utilitzant els millors hiperparàmetres trobats.
- Avaluar només una vegada sobre el test set per obtenir l’estimació final i honesta de l’error de generalització.
Aquest enfocament combina el millor dels dos mons: la robustesa de K-Fold per a la selecció de model, i la regla d’or de mantenir el test set intacte fins al final.
Biaix i variància
Analitzar el biaix i la variància d’un model ens dóna una bona idea de què provar quan el model no funciona bé.
Per exemple, considerem un model polinòmic per ajustar un conjunt de dades:
- Alt biaix: el model no s’ajusta bé al conjunt de dades (underfitting).
- Alta variància: el model s’ajusta massa al conjunt de dades i no generalitza bé (overfitting).
- Model correcte: trobem un punt intermedi amb biaix i variància equilibrats.
Si només tenim una característica (feature), es pot visualitzar directament com el model s’ajusta al conjunt de dades.
Amb més característiques, hem d’analitzar el rendiment del model sobre:
- Training set (J_{\text{train}})
- Cross-validation set (J_{\text{cv}})
Comportament segons l’ajustament del model:
| Tipus de model | J_{\text{train}} | J_{\text{cv}} |
|---|---|---|
| Subajustament (alt biaix) | Alt | Alt |
| Sobreajustament (alta variància) | Baix | Alt |
| Correcte | Baix | Baix |
Imaginem que tenim un dataset que modelem com un polinomi, i volem representar gràficament l’error d’entrenament i verificació en funció de d:
Observacions:
- J_{\text{train}} sempre disminueix quan augmenta d.
- J_{\text{cv}} té forma aproximadament quadràtica, amb un mínim en el model “correcte” (d = 3).
És un cas menys freqüent, però per a models molt complexos (per exemple, xarxes neuronals), és possible trobar simultàniament Alt biaix i alta variància. Això passa perquè el model s’ajusta massa per a algunes regions de les dades (overfitting) i massa poc per a altres (underfitting).
Resumint, l’objectiu és trobar un equilibri que minimitzi ambdós errors.
Regularització
Partim d’un model de regressió polinòmica:
f_{w,b}(x) = w_1 x + w_2 x^2 + w_3 x^3 + w_4 x^4 + b
Amb la funció de cost:
J(w, b) = J_\text{original}(w, b) + \frac{\lambda}{2m} \sum_{j=1}^{n} w_j^2
- Si fem que \lambda sigui molt gran, l’entrenament farà que els pesos siguin propers a zero, i per tant: f_{w,b}(x) \approx b , que és un model amb alt biaix (subajustament) on J_{train} és gran.
- Si fem que \lambda sigui molt petit llavors no hi ha regularització i el model té alta variància (sobreajustament) on J_{train} és petit i J_{cv} és gran.
- L’objectiu és tenir un valor intermedi de \lambda que faci J_{train} i J_{cv} petits.
El procediment per calcular el millor valor de \lambda serà trobar l’error de validació J_{cv} més petit per a \lambda = 0 \ldots \text{max} . En el nostre cas, farem max = 100.
Observacions:
- J_{\text{train}} sempre augmenta quan augmenta \lambda.
- J_{\text{cv}} té forma aproximadament quadràtica, amb un mínim en el model “correcte” (\lambda = 1).
Regularització en xarxes neuronals
En xarxes neuronals, la regularització L2 s’implementa mitjançant el paràmetre weight_decay de l’optimitzador. Aquest paràmetre és equivalent al paràmetre de regularització \lambda que hem vist abans.
En PyTorch, aquest terme es gestiona automàticament amb weight_decay:
import torch.optim as optim
# weight_decay és equivalent a λ
optimizer = optim.Adam(model.parameters(), lr=0.001, weight_decay=0.01)
Valors típics de weight_decay:
weight_decay=0: Sense regularització (pot causar overfitting)weight_decay=0.01: Regularització moderada (valor comú)weight_decay=0.1: Regularització forta (per models molt complexos)
Notes:
- Usa
Adamper a la majoria de casos AdamWés una variant que implementa weight decay de forma més correcta (recomanat quan uses regularització L2)weight_decayaplica la penalització a tots els paràmetres del model- Com amb \lambda, cal ajustar
weight_decayamb validació creuada
Rendiment de referència
Quan dissenyem un model, és important preguntar-nos: quin nivell d’error podem esperar assolir raonablement? Per establir una baseline (referència inicial) es poden tenir en compte diversos factors:
- Rendiment humà: especialment en dades no estructurades (per exemple, imatges o veu).
- Rendiment d’algoritmes competidors: comparar amb models existents similars.
- Estimació basada en experiència: quan no hi ha comparatives directes, utilitzar coneixement previ.
Nota: la baseline podria ser 0% d’error, però en aplicacions sorolloses com el reconeixement de veu, el valor de referència pot ser més alt.
Què cal comprovar? Abans de fer ajustos complexos, hem de mesurar:
- Baseline performance: el punt de referència inicial.
- Training error: l’error sobre les dades d’entrenament.
- Cross-validation error: l’error sobre dades de validació (dades no vistes durant l’entrenament).
Un cop mesurades aquestes mètriques, podem analitzar possibles problemes:
-
Alt biaix:
- Diferència gran entre baseline i training error → el model no aprèn bé ni sobre l’entrenament.
-
Alta variància:
- Diferència gran entre training error i cross-validation error → el model s’ajusta massa al conjunt d’entrenament i no generalitza bé.
Aquest enfocament ajuda a identificar on cal actuar per millorar el model: si reduir la complexitat, afegir més dades, o regularitzar.
Curves d’aprenentatge
Considerem un model de regressió polinòmic de segon grau:
f_{w,b}(x) = w_1 x + w_2 x^2 + b
Si representem la mida del conjunt d’entrenament m_\text{train} a l’eix horitzontal i l’error a l’eix vertical, obtenim les següents corbes:
- J_{train} comença baixa i augmenta a mesura que incrementem la mida del conjunt, ja que els polinomis poden ajustar-se bé amb pocs exemples.
- J_{cv} comença alta i disminueix a mesura que el model generalitza millor amb més dades d’entrenament.
Aquestes corbes mostren la intuïció clàssica de les curves d’aprenentatge, on el model tendeix a sobreajustar amb poques dades i millora la seva capacitat de generalització a mesura que la mida del conjunt creix.
Què passa en situacions d’alt biaix i alta variància?
-
Alt biaix:
- El model és massa simple per capturar la complexitat de les dades.
- J_{train} serà alt i J_{cv} també serà alt, amb poc espai entre elles i per sobre del rendiment de referència.
- Les corbes no baixen gaire amb més dades, fins i tot queden planes. Cal considerar canviar altres coses.
-
Alta variància:
- El model és molt complex respecte a la quantitat de dades.
- J_{train} serà molt baix, mentre que J_{cv} serà molt més alt, creant una gran separació entre les corbes. El rendiment de referència pot quedar al mig.
- A mesura que afegim més dades d’entrenament, J_{cv} disminuirà gradualment, i per tant és una forma de resoldre aquest problema.
Què podem fer (revisat)
Revisem els sis remeis que vam veure, i indicarem quin problema adrecen:
- Obtenir més exemples d’entrenament: alta variància.
- Reduir el conjunt de característiques (features): alta variància.
- Afegir noves característiques: alt biaix.
- Afegir característiques polinòmiques (per exemple, x_1^2, x_2^2, \ldots ): alt biaix.
- Disminuir el paràmetre de regularització \lambda: alt biaix.
- Incrementar el paràmetre de regularització \lambda: alta variància.
Com es pot veure, reduir els exemples d’entrenament no s’inclou a la llista de remeis. No resol cap dels dos problemes.
Biaix i variància en xarxes neuronals
El tradeoff de biaix i variància és el concepte que, triant adequadament l’ordre del polinomi o el millor paràmetre de regularització, podem aconseguir que el model sigui tan precís com sigui possible sense sobreajustar-se.
En el cas de les xarxes neuronals, aquest tradeoff funciona de manera diferent. Les xarxes grans entrenades amb datasets relativament petits solen ser màquines de baix biaix. Això dóna lloc a una recepta pràctica que, tot i no funcionar sempre, pot ajudar a trobar un model precís:
- Entrena l’algorisme amb el teu conjunt d’entrenament.
- Mesura J_{train} i comprova si és alt respecte al nivell de referència (indicant alt biaix).
- Si hi ha alt biaix, utilitza una xarxa neuronal més gran (més capes ocultes o més unitats per capa) i torna al pas 1.
- Si no, mesura J_{cv} i comprova si és alt (indicant alta variància).
- Si hi ha alta variància, obté més dades i torna al pas 1.
- Si no, el model és satisfactori i has acabat.
Limitacions d’aquesta recepta:
- Augmentar la mida de la xarxa neuronal: Tot i l’existència de GPUs, arriba un punt en què ja no és possible engrandir la xarxa. Tot i que això podria incrementar la variància, amb una bona regularització una xarxa més gran serà almenys tan bona com una més petita.
- Obtenir més dades: No sempre és possible aconseguir-les.
Altres tècniques
A més de la regularització L2 (paràmetre \lambda) que hem vist anteriorment, existeixen altres tècniques efectives per combatre l’alta variància (overfitting):
Dropout
Dropout és una tècnica de regularització específica per a xarxes neuronals que ajuda a prevenir el sobreajustament.
Com funciona:
Durant l’entrenament, dropout “apaga” aleatòriament un percentatge de neurones a cada iteració. Per exemple, amb un dropout rate de 0.5, cada neurona té un 50% de probabilitat de ser temporalment desactivada.
- Durant l’entrenament: A cada forward pass, es desactiven aleatòriament neurones segons el dropout rate.
- Durant la inferència: Totes les neurones estan actives, però els seus pesos s’escalen pel dropout rate per compensar.
Per què funciona:
- Evita que les neurones es “co-adaptin” massa (és a dir, que depenguin massa les unes de les altres).
- Força la xarxa a aprendre representacions més robustes i redundants.
- Equivalent a entrenar un ensemble de moltes xarxes diferents i fer-ne la mitjana.
Quan usar-lo:
- Quan tens alta variància: J_{train} baix però J_{cv} alt.
- En xarxes neuronals grans amb molts paràmetres.
- Especialment efectiu en capes totalment connectades (fully connected).
Valors típics:
- Dropout rate de 0.2-0.5 per a capes ocultes.
- Dropout rate de 0.5 és un valor estàndard i comú.
- Per a capes convolucionals, valors més baixos (0.1-0.3) solen funcionar millor.
Exemple amb PyTorch:
import torch
import torch.nn as nn
import torch.nn.functional as F
class MyNetwork(nn.Module):
def __init__(self, input_size=784, hidden1=128, hidden2=64, num_classes=10, dropout_rate=0.5):
super().__init__()
self.fc1 = nn.Linear(input_size, hidden1)
self.dropout1 = nn.Dropout(p=dropout_rate)
self.fc2 = nn.Linear(hidden1, hidden2)
self.dropout2 = nn.Dropout(p=0.3)
self.fc3 = nn.Linear(hidden2, num_classes)
def forward(self, x):
x = F.relu(self.fc1(x))
x = self.dropout1(x)
x = F.relu(self.fc2(x))
x = self.dropout2(x)
x = self.fc3(x)
return x
# Ús del model
model = MyNetwork(dropout_rate=0.5)
model.train() # Activa dropout durant entrenament
# ... entrenament ...
model.eval() # Desactiva dropout durant inferència
Early Stopping
Early stopping és una tècnica simple però efectiva per prevenir el sobreajustament que funciona amb qualsevol tipus de model entrenat de forma iterativa (xarxes neuronals, gradient boosting, etc.).
Com funciona:
Durant l’entrenament, monitoritzes l’error de validació J_{cv} . Quan aquest error deixa d’millorar (o comença a augmentar), atures l’entrenament i recuperes els pesos del model que tenien el millor rendiment de validació.
Procediment:
- Divideix les dades en train, validation i test.
- Entrena el model i, després de cada època, calcula J_{cv} .
- Guarda els pesos del model quan J_{cv} és el més baix.
- Si J_{cv} no millora durant N èpoques consecutives (patience), atura l’entrenament.
- Restaura els pesos guardats del millor model.
Per què funciona:
- Evita que el model continuï entrenant fins al punt de memoritzar les dades d’entrenament.
- Troba automàticament el punt òptim entre underfitting i overfitting.
- És una forma de regularització “gratuïta” que no afegeix hiperparàmetres complexos.
Quan usar-lo:
- Quan tens alta variància: J_{train} disminueix però J_{cv} comença a augmentar.
- Com a complement o alternativa a altres tècniques de regularització.
- Quan vols estalviar temps de computació (atura abans que l’entrenament complet).
Paràmetre clau:
- Patience: Nombre d’èpoques a esperar sense millora abans d’aturar. Valors típics: 5-20 èpoques.
Exemple amb PyTorch:
import torch
import copy
def train_with_early_stopping(model, train_loader, val_loader,
optimizer, criterion, patience=10, max_epochs=100):
best_val_loss = float('inf')
patience_counter = 0
for epoch in range(max_epochs):
# Entrenament
model.train()
for X_batch, y_batch in train_loader:
optimizer.zero_grad()
loss = criterion(model(X_batch), y_batch)
loss.backward()
optimizer.step()
# Validació
model.eval()
val_loss = 0.0
with torch.no_grad():
for X_val, y_val in val_loader:
val_loss += criterion(model(X_val), y_val).item()
val_loss /= len(val_loader)
# Early stopping
if val_loss < best_val_loss:
best_val_loss = val_loss
best_model_state = copy.deepcopy(model.state_dict())
patience_counter = 0
else:
patience_counter += 1
if patience_counter >= patience:
print(f'Early stopping a època {epoch+1}')
model.load_state_dict(best_model_state)
break
return model
Comparació amb regularització L2:
- Early stopping: Controla quan aturar l’entrenament.
- Regularització L2 (\lambda): Controla la magnitud dels pesos durant tot l’entrenament.
- Es poden combinar ambdues tècniques per millors resultats.
Avantatges d’early stopping:
- No requereix ajustar hiperparàmetres complexos (només patience).
- Estalvia temps de computació.
- Funciona amb qualsevol algoritme iteratiu.
Inconvenients:
- Requereix un conjunt de validació separat.
- Pot aturar massa aviat si el patience és massa baix.
Resum
Ambdues tècniques adrecen el problema d’alta variància:
| Tècnica | Aplicable a | Paràmetre clau | Quan usar-la |
|---|---|---|---|
| Dropout | Només xarxes neuronals | Dropout rate (0.2-0.5) | J_{train} baix, J_{cv} alt |
| Early stopping | Qualsevol model iteratiu | Patience (5-20 èpoques) | J_{train} disminueix, J_{cv} augmenta |
Aquestes tècniques es poden combinar amb regularització L2 i altres mètodes per obtenir models més robustos i amb millor capacitat de generalització.
Arbres de decisió
- Introducció
- Què és un arbre de decisió?
- Diversos arbres possibles
- Construcció d’un arbre de decisió
- Com escollir la millor pregunta?
- Algorisme de l’arbre de decisió
- Codificació One-Hot
- Característiques contínues
- Arbres de decisió per a regressió
- Hiperparàmetres: controlant la complexitat
- Importància de les característiques
- Conjunts d’arbres (Ensembles)
- Quan usar arbres vs xarxes neuronals?
Introducció
Per entendre com funcionen els arbres de decisió, farem servir un exemple senzill:
Suposeu que gestioneu un centre d’adopció de gats i que, donades algunes característiques d’un animal, voleu entrenar un classificador que decideixi ràpidament si es tracta d’un gat o no.
Disposem de 10 exemples d’entrenament. Per a cada animal tenim les característiques següents:
- Forma de les orelles (punxegudes o caigudes)
- Forma de la cara (rodona o no rodona)
- Bigotis (present o absent)
- Etiqueta: és gat (1) o no és gat (0)

Exemple:
- El primer animal té orelles punxegudes, cara rodona, bigotis presents → és un gat.
- El segon té orelles caigudes, cara no rodona, bigotis presents → també és un gat.
- I així successivament fins a completar els 10 exemples (5 gats i 5 gossos).
Formalment:
- Les entrades X són les tres columnes de característiques.
- La sortida Y és la columna final que indica si és gat o no.
Com que Y \in {0,1} , es tracta d’un problema de classificació binària.
En aquest exemple, cada característica X_1, X_2, X_3 només pot prendre dos valors possibles (categòrics). Més endavant veurem com treballar amb característiques multivaluades o fins i tot contínues.
Què és un arbre de decisió?
Un arbre de decisió és un model que, després d’entrenar-se amb dades, pren la forma d’una estructura jeràrquica anomenada arbre.
- Cada oval dins l’arbre és un node de decisió.
- El node arrel (a dalt de tot) és el punt de partida per classificar qualsevol exemple.
- Les fulles (caixes rectangulars a baix) representen les prediccions finals.
Funcionament
Si entra un nou exemple (p. ex., animal amb orelles punxegudes, cara rodona i bigotis presents), el procés és:
- Comencem al node arrel, que pot preguntar: “Quina és la forma de les orelles?”.
- Segons la resposta (punxegudes/caigudes), anem cap a l’esquerra o cap a la dreta.
- Al següent node, potser es pregunta: “La cara és rodona?”.
- Continuem baixant per l’arbre fins arribar a una fulla, que dona la predicció: “És un gat”.
👉 Encara que sembli estrany que les “arrels” siguin a dalt i les “fulles” a baix, penseu-ho com una planta penjant d’interior.
Diversos arbres possibles
Un mateix conjunt de dades pot donar lloc a molts arbres diferents.
Per exemple, podem tenir:
- Un arbre que primer mira les orelles i després els bigotis.
- Un altre que primer mira la cara i després les orelles.
- Altres variants amb criteris diferents.
Cadascun d’aquests arbres pot tenir un rendiment millor o pitjor sobre el conjunt d’entrenament, validació o test. La tasca de l’algoritme d’aprenentatge és trobar, entre tots els arbres possibles, un que generalitzi bé: és a dir, que tingui bon rendiment no només amb les dades vistes, sinó també amb noves dades.
Resumint:
- Els arbres de decisió permeten classificar exemples a partir d’un procés seqüencial de preguntes sobre les característiques.
- Cada node representa una decisió basada en una característica.
- Cada fulla representa una predicció final.
- Podem construir molts arbres diferents amb el mateix conjunt de dades; el repte és trobar-ne un que funcioni bé en general.
Construcció d’un arbre de decisió
El procés de construir un arbre de decisió a partir d’un conjunt d’entrenament es pot entendre en diversos passos. Vegem-ne el funcionament amb un exemple senzill de 10 mostres de gats i gossos.
En el següent arbre s’ha afegit el nombre d’animals que coincideixen amb el criteri de decisió (entre parèntesis):

Pas 1. Escollir la característica de l’arrel
El primer pas és decidir quina característica utilitzarem al node arrel (el node superior de l’arbre).
Mitjançant un algoritme (que veurem més endavant), suposem que escollim la forma de les orelles.
- Els 5 exemples amb orelles punxegudes van al subarbre esquerre.
- Els 5 exemples amb orelles caigudes van al subarbre dret.
Pas 2. Dividir el subarbre esquerre
Ara ens fixem només en les 5 mostres amb orelles punxegudes. Hem de decidir una nova característica per dividir.
Imaginem que escollim la forma de la cara.
- 4 mostres tenen cara rodona → baixen a l’esquerra.
- 1 mostra té cara no rodona → baixa a la dreta.
Com que les 4 mostres amb cara rodona són totes gats, aquest node es converteix en un node fulla que preveu “gat”.
El node de la dreta conté un gos (100%), així que també es crea un node fulla que preveu “no gat”.
Pas 3. Dividir el subarbre dret
Passem al subarbre dret (les 5 mostres amb orelles caigudes). Aquí hi ha 1 gat i 4 gossos.
Suposem que escollim la característica bigotis.
- Si hi ha bigotis → queda 1 gat (100%).
- Si no hi ha bigotis → queden 4 gossos (100%).
Tots dos nodes són purs (conté només una classe), de manera que es converteixen en fulles: “gat” i “no gat”.
Decisions clau en l’aprenentatge d’arbres de decisió
Durant el procés hem de prendre diverses decisions importants:
1. Quina característica escollir en cada node?
En cada node amb una barreja de gats i gossos, l’algoritme ha de decidir quina característica és millor per dividir:
- forma de les orelles,
- forma de la cara,
- bigotis, etc.
La idea és maximitzar la puresa de les divisions.
Per exemple, si tinguéssim la característica fictícia “té ADN de gat”, dividir per ella produiria subconjunts totalment purs (100% gats a l’esquerra i 0% gats a la dreta).
En general, triem la característica que produeix subconjunts més homogenis.
2. Quan aturar la divisió?
No podem dividir indefinidament. Els criteris típics per aturar la divisió són:
- Puresa total: si un node conté només una classe (100% gats o 100% gossos), es crea una fulla.
- Profunditat màxima: limitem l’arbre a una profunditat determinada.
- La profunditat d’un node és el nombre de salts des de l’arrel.
- Exemple: si imposem una profunditat màxima de 2, cap node pot arribar a profunditat 3.
- Mida mínima del node: si un node té molt poques mostres (p. ex. només 3), es prefereix no dividir més.
- Guany de puresa insuficient: si la millora en puresa (o reducció d’impuresa) és molt petita, no val la pena dividir.
Aquests criteris ajuden a reduir el risc de sobreajustament i a mantenir l’arbre manejable.
Reflexió final
Els arbres de decisió poden semblar complicats perquè, amb els anys, molts investigadors han anat afegint refinaments:
- nous criteris de divisió,
- límits de profunditat,
- criteris de parada alternatius, etc.
Però, en essència, només hi ha dues preguntes clau:
- Quina característica faig servir per dividir?
- Quan em paro de dividir?
Amb aquestes regles, més una bona mesura de puresa (com l’entropia), podem construir arbres de decisió efectius.
Com escollir la millor pregunta?
Quan construïm un arbre de decisió, en cada node hem de decidir quina característica utilitzar per dividir. La intuïció és senzilla: volem preguntes que separin bé els exemples.
Imagina que tens una bossa amb gats i gossos barrejats. Una bona pregunta és aquella que, en respondre-la, et deixa dues bosses on els animals són més semblants entre si (idealment, tots gats a una banda i tots gossos a l’altra).
Mesurar la “barreja”: l’entropia
Per quantificar com de barrejat està un conjunt, fem servir l’entropia:
- Conjunt pur (tots gats o tots gossos) → entropia = 0
- Conjunt 50%-50% → entropia = 1 (màxima confusió)
L’entropia es calcula amb la fórmula H(p) = -p \log_2(p) - (1-p)\log_2(1-p), on p és la proporció d’una classe. Però el que importa és la intuïció: com més barrejat, més alta l’entropia.
Exemple: quina característica triem?
Tornem al nostre exemple amb 10 animals (5 gats i 5 gossos). Si provem les tres característiques:
| Característica | Esquerra | Dreta | Resultat |
|---|---|---|---|
| Orelles | 4 gats, 1 gos | 1 gat, 4 gossos | Bona separació |
| Cara | 4 gats, 3 gossos | 1 gat, 2 gossos | Encara barrejats |
| Bigotis | 3 gats, 1 gos | 2 gats, 4 gossos | Regular |
Visualment és clar: “orelles” deixa els grups més purs. Això es reflecteix en el guany d’informació (la reducció d’entropia que aconseguim amb cada divisió):
- Orelles: 0.28 ← màxim guany
- Bigotis: 0.12
- Cara: 0.03
Triem sempre la característica amb el guany més alt.
Criteri d’aturada
El guany d’informació també ens indica quan parar: si el guany és molt petit, no val la pena continuar dividint (només augmentaríem la complexitat de l’arbre sense millorar gaire la precisió).
Nota: Existeixen alternatives a l’entropia, com el criteri de Gini que veurem a continuació. A la pràctica, ambdues mesures donen resultats similars.
Criteri de Gini
El criteri de Gini (o impuresa de Gini) és una altra manera de mesurar com de barrejat està un conjunt. La intuïció és senzilla:
Si agafes dos elements a l’atzar del conjunt, quina probabilitat hi ha que siguin de classes diferents?
- Conjunt pur (tots gats o tots gossos) → Gini = 0 (mai agafaràs dos de classes diferents)
- Conjunt 50%-50% → Gini = 0.5 (màxima barreja)
| Mesura | Rang | Màxim a | Què mesura |
|---|---|---|---|
| Entropia | 0 a 1 | 50%-50% | “Sorpresa” o incertesa |
| Gini | 0 a 0.5 | 50%-50% | Probabilitat d’error aleatori |
Ambdues mesures arriben al mínim (0) quan el conjunt és pur, i al màxim quan hi ha màxima barreja. Per tant, donen resultats molt similars a l’hora d’escollir la millor característica per dividir.
Per què scikit-learn usa Gini per defecte?
- És lleugerament més ràpid de calcular (no requereix logaritmes)
- A la pràctica, la diferència de resultats és mínima
Pots canviar el criteri fàcilment:
from sklearn.tree import DecisionTreeClassifier
# Exemple amb profunditat màxima 3
model = DecisionTreeClassifier(max_depth=3, random_state=42)
# Es podria utilitzar gini o entropy amb:
# model = DecisionTreeClassifier(max_depth=3, random_state=42, criterion='gini')
# model = DecisionTreeClassifier(max_depth=3, random_state=42, criterion='entropy')
model.fit(X_train, y_train)
predictions = model.predict(X_test)
Algorisme de l’arbre de decisió
L’algorisme és recursiu: apliquem el mateix procés a cada node fins que parem.
funció construir_arbre(exemples):
si tots els exemples són de la mateixa classe:
retorna fulla amb aquesta classe
característica = la que té màxim guany d'informació
dividir exemples segons característica
retorna node(
esquerra = construir_arbre(exemples_esquerra),
dreta = construir_arbre(exemples_dreta)
)
Quan parem?
- El node ja és pur (tots de la mateixa classe)
- S’ha arribat a la profunditat màxima
- El guany d’informació és massa petit
- Queden massa pocs exemples per dividir
Les biblioteques com scikit-learn proporcionen valors per defecte raonables per aquests paràmetres.
Codificació One-Hot
Què passa si una característica té més de dos valors? Per exemple, les orelles poden ser punxegudes, caigudes o ovalades.
La solució és crear una columna per cada valor possible:
| Animal | Orelles (original) | Punxegudes | Caigudes | Ovalades | |
|---|---|---|---|---|---|
| Gat 1 | punxegudes | 1 | 0 | 0 | |
| Gat 2 | ovalades | 0 | 0 | 1 | |
| Gos 1 | caigudes | 0 | 1 | 0 |
Això s’anomena one-hot perquè exactament una columna està “encesa” (val 1) per cada fila.
Per què és útil?
Molts algoritmes (xarxes neuronals, regressió lineal/logística) fan operacions matemàtiques amb les dades: multiplicacions, sumes, derivades… Però no podem multiplicar “punxegudes” per un pes. Necessitem números.
La codificació one-hot resol això convertint cada categoria en un vector de 0s i 1s que sí es pot operar matemàticament.
A la pràctica, la majoria de biblioteques ho fan automàticament:
import pandas as pd
# Dades d'exemple
df = pd.DataFrame({
'orelles': ['punxegudes', 'ovalades', 'caigudes']
})
# Codificació one-hot amb pandas
encoded = pd.get_dummies(df, columns=['orelles'])
print(encoded)
# orelles_caigudes orelles_ovalades orelles_punxegudes
# 0 0.0 0.0 1.0
# 1 0.0 1.0 0.0
# 2 1.0 0.0 0.0
Característiques contínues
Fins ara hem vist arbres de decisió amb característiques discretes (orelles punxegudes/caigudes, bigotis sí/no). Però què passa si tenim una característica contínua com el pes de l’animal?
De mitjana, els gats solen ser més lleugers que els gossos. Així doncs, el pes és una característica útil per classificar.
La idea: buscar un llindar
Amb característiques contínues, la pregunta ja no és “té orelles punxegudes?” sinó “pesa menys de X quilos?”. L’algoritme ha de trobar el millor valor de tall.
Exemple: on tallem?
Imaginem que provem diferents llindars:
| Llindar | Esquerra (≤) | Dreta (>) | Qualitat |
|---|---|---|---|
| pes ≤ 8 | 2 gats | 3 gats, 5 gossos | Dolenta (dreta molt barrejada) |
| pes ≤ 9 | 4 gats | 1 gat, 5 gossos | Bona (grups més purs) |
| pes ≤ 13 | 4 gats, 2 gossos | 1 gat, 3 gossos | Regular |
El llindar pes ≤ 9 és el millor: deixa tots els gats lleugers a l’esquerra i gairebé tots els gossos a la dreta.
Com ho fa l’algoritme?
En lloc de provar valors a l’atzar, l’algoritme:
- Ordena els exemples per pes.
- Prova cada punt mitjà entre exemples consecutius com a possible llindar.
- Escull el llindar que produeix els grups més purs (màxim guany d’informació).
Amb 10 exemples, es proven 9 possibles llindars i es queda amb el millor.
Resum
Les característiques contínues es tracten igual que les discretes: l’algoritme busca la divisió que maximitzi la puresa. L’única diferència és que, en lloc de preguntar per categories, pregunta “és menor o igual que X?” per algun valor X que s’ha trobat òptim.
Arbres de decisió per a regressió
Fins ara hem utilitzat arbres de decisió per classificar (gat o gos). Però també poden predir valors numèrics, com ara el pes d’un animal. Això s’anomena regressió.
Què canvia respecte a la classificació?
| Classificació | Regressió | |
|---|---|---|
| Predicció | La classe més freqüent (vot majoritari) | La mitjana dels valors |
| Criteri de divisió | Reduir l’entropia (barreja) | Reduir la variància (dispersió) |
Com funciona?
Imaginem que volem predir el pes d’un animal segons la forma de les orelles i la cara.
- L’arbre divideix els exemples igual que abans.
- A cada fulla, en lloc d’una etiqueta, tenim un conjunt de pesos dels animals d’entrenament.
- La predicció és la mitjana d’aquests pesos.
Exemple: Si una fulla conté animals amb pesos 7.2, 7.6, 10.2 i 8.4 kg, la predicció serà 8.35 kg (la mitjana).
Per què variància en lloc d’entropia?
En classificació, un bon tall separa gats de gossos (redueix la “barreja”).
En regressió, un bon tall agrupa animals amb pesos similars:
- Valors 7, 8, 9 kg → molt agrupats → bona predicció (la mitjana és representativa)
- Valors 5, 12, 20 kg → molt dispersos → mala predicció (la mitjana no representa cap valor real)
L’algoritme busca talls que minimitzin aquesta dispersió (variància) a cada subgrup.
En poques paraules
La mecànica és idèntica a la classificació:
- Dividir segons la característica que més redueixi la variància
- Parar quan el node és prou homogeni o s’arriba al límit
- Predir amb la mitjana dels valors a cada fulla
Hiperparàmetres: controlant la complexitat
Els hiperparàmetres són els “botons de control” que determinen com de complex serà l’arbre. Ajustar-los bé és clau per evitar tant el sobreajustament (arbre massa complex) com el subajustament (arbre massa simple).
Els principals hiperparàmetres
| Hiperparàmetre | Què controla | Efecte si és alt | Efecte si és baix |
|---|---|---|---|
max_depth | Profunditat màxima | Arbre complex, risc de sobreajustament | Arbre simple, risc de subajustament |
min_samples_split | Mínim d’exemples per dividir un node | Divisions més conservatives | Més divisions, arbre més complex |
min_samples_leaf | Mínim d’exemples a cada fulla | Fulles més “segures” | Fulles poden tenir pocs exemples |
La intuïció: profunditat i sobreajustament
Imagina un arbre amb profunditat il·limitada:
- Pot créixer fins que cada fulla contingui un sol exemple
- Aconseguirà 100% accuracy en entrenament (memoritza les dades)
- Però fallarà amb dades noves (no ha après patrons generals)
És com estudiar un examen memoritzant les respostes exactes en lloc d’entendre els conceptes.
Exemple pràctic:
| Profunditat | Train accuracy | Test accuracy | Diagnòstic |
|---|---|---|---|
| 2 | 75% | 74% | Subajustament (massa simple) |
| 5 | 92% | 88% | Bon equilibri |
| 20 | 100% | 72% | Sobreajustament (memoritza) |
Com trobar els valors òptims?
La millor estratègia és validació creuada: provar diferents combinacions i veure quina funciona millor amb dades de validació.
La idea és senzilla: dividim les dades en k parts (típicament 5). Entrenem el model k vegades, cada vegada deixant una part diferent per validar. Així obtenim k mesures de rendiment que podem fer la mitjana.
Per què és millor que una sola divisió train/test?
- Amb una sola divisió, el resultat depèn de quins exemples han quedat a cada costat (sort)
- Amb k divisions rotatives, cada exemple serveix tant per entrenar com per validar
- El resultat és més fiable i estable, especialment amb datasets petits
Per una explicació més detallada, consulta la secció K-Fold Cross-Validation.
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import GridSearchCV
# Definir el rang de valors a provar
param_grid = {
'max_depth': [3, 5, 10, 20, None],
'min_samples_split': [2, 5, 10],
'min_samples_leaf': [1, 2, 4]
}
# Cercar la millor combinació (cv=5 significa k=5 folds)
tree = DecisionTreeClassifier(random_state=42)
search = GridSearchCV(tree, param_grid, cv=5, scoring='accuracy')
search.fit(X_train, y_train)
print(f"Millors paràmetres: {search.best_params_}")
print(f"Millor score: {search.best_score_:.3f}")
Consells pràctics
- Comença simple:
max_depth=5és un bon punt de partida - Observa la diferència train/test: si train >> test, l’arbre és massa complex
- Per datasets petits: limita més la profunditat (menys dades = més risc de sobreajustar)
Nota sobre poda (pruning): Existeix una tècnica alternativa anomenada post-pruning, que deixa créixer l’arbre completament i després talla branques innecessàries. A la pràctica, controlar la complexitat amb hiperparàmetres (com fem aquí) és més comú i sol ser suficient. Si necessites simplificar un arbre ja entrenat, scikit-learn ofereix el paràmetre
ccp_alpha.
Importància de les característiques
Un dels grans avantatges dels arbres de decisió és que ens diuen quines característiques són més útils per fer prediccions. Això s’anomena feature importance (importància de les característiques), i mesura quanta impuresa elimina una característica en el total de l’arbre.
La intuïció
Recorda que a cada node l’arbre escull la característica que més redueix la impuresa (entropia o Gini). Si una característica:
- S’usa a prop de l’arrel → afecta molts exemples → és molt important
- S’usa només a les fulles → afecta pocs exemples → és menys important
- No s’usa mai → importància = 0
Exemple pràctic
Tornem al nostre classificador de gats. Després d’entrenar, podem veure la importància de cada característica:
| Característica | Importància |
|---|---|
| Forma orelles | 0.52 |
| Bigotis | 0.31 |
| Forma cara | 0.17 |
Això ens diu que la forma de les orelles és el factor més determinant per distingir gats de gossos en les nostres dades.
Com obtenir-ho amb codi
from sklearn.tree import DecisionTreeClassifier
import pandas as pd
# Entrenar el model
model = DecisionTreeClassifier(max_depth=5, random_state=42)
model.fit(X_train, y_train)
# Obtenir la importància de cada característica
importances = pd.DataFrame({
'característica': feature_names,
'importància': model.feature_importances_
}).sort_values('importància', ascending=False)
print(importances)
Per a què serveix?
- Entendre el model: saber què mira l’arbre per decidir
- Explicar prediccions: “et classifiquem com a risc alt principalment pel teu historial de crèdit”
- Selecció de característiques: eliminar les que tenen importància zero per simplificar
- Detectar problemes: si una característica inesperada és molt important, potser hi ha un error a les dades
Visualitzar l’arbre
També pots dibuixar l’arbre complet per veure exactament quines decisions pren:
from sklearn.tree import plot_tree
import matplotlib.pyplot as plt
plt.figure(figsize=(15, 8))
plot_tree(
model,
feature_names=feature_names,
class_names=['gos', 'gat'],
filled=True,
rounded=True
)
plt.show()
Nota: La visualització només és útil per arbres petits. Amb
max_depth > 5, l’arbre es fa difícil de llegir.
Conjunts d’arbres (Ensembles)
Un ensemble és fer treballar molts models junts per obtenir millors resultats. En lloc de confiar en un sol arbre de decisió, entrenem molts arbres i els fem votar.
Per què un sol arbre no és suficient?
Un arbre de decisió és molt sensible a petits canvis en les dades. Si canviem un sol exemple del conjunt d’entrenament, la característica escollida al node arrel pot canviar, generant un arbre totalment diferent.
La solució: entrenar molts arbres diferents i combinar les seves prediccions. Així, cap arbre individual té massa influència sobre el resultat final.
Com funciona la votació?
Imaginem un ensemble de 3 arbres classificant un animal:
| Arbre | Predicció |
|---|---|
| Arbre 1 | gat |
| Arbre 2 | gos |
| Arbre 3 | gat |
Resultat: 2 vots gat, 1 vot gos → gat guanya per majoria.
Com creem arbres diferents?
Si entrenem tots els arbres amb les mateixes dades, obtindrem arbres idèntics. Necessitem variació. Hi ha dues tècniques:
Bagging (mostreig amb reposició)
Per cada arbre, creem un conjunt d’entrenament lleugerament diferent:
- Posem tots els exemples en una “bossa”
- Traiem exemples a l’atzar, tornant-los a posar cada vegada
- Repetim fins tenir el mateix nombre d’exemples
El resultat: alguns exemples es repeteixen, altres no apareixen.
| Dades originals | Mostra 1 | Mostra 2 | Mostra 3 |
|---|---|---|---|
| A, B, C, D, E | A, A, C, D, E | B, C, C, D, E | A, B, D, D, E |
Selecció aleatòria de característiques
A cada node, en lloc de considerar totes les característiques, només en considerem un subconjunt aleatori (típicament √n). Això força els arbres a explorar camins diferents.
Random Forest
Random Forest combina ambdues tècniques:
- Crear ~100 arbres, cadascun entrenat amb un mostreig diferent de les dades
- A cada node, considerar només un subconjunt aleatori de característiques
- Per predir: cada arbre vota, guanya la majoria
Això produeix arbres diversos que, combinats, són molt més robustos que un sol arbre.
from sklearn.ensemble import RandomForestClassifier
rf = RandomForestClassifier(n_estimators=100, max_depth=5, random_state=42)
rf.fit(X_train, y_train)
predictions = model.predict(X_test)
XGBoost (Boosting)
XGBoost utilitza una estratègia diferent: en lloc d’arbres independents, cada arbre corregeix els errors dels anteriors.
La idea és com la pràctica deliberada al piano: en lloc de tocar tota la peça repetidament, et concentres en els compassos que encara no domines.
| Pas | Què fa |
|---|---|
| 1 | Entrenar el primer arbre normalment |
| 2 | Identificar els exemples mal classificats |
| 3 | Entrenar el següent arbre donant més pes als errors |
| 4 | Repetir fins tenir ~100 arbres |
Random Forest vs XGBoost
| Random Forest | XGBoost | |
|---|---|---|
| Idea | Arbres independents que voten | Arbres que corregeixen errors anteriors |
| Entrenament | Paral·lel (més ràpid) | Seqüencial |
| Quan usar | Bon punt de partida | Quan necessites màxima precisió |
A la pràctica, XGBoost sol donar millors resultats i és l’algoritme dominant en competicions de ML i aplicacions comercials.
from xgboost import XGBClassifier
# Classificació
model = XGBClassifier(n_estimators=100, max_depth=5, random_state=42)
model.fit(X_train, y_train)
predictions = model.predict(X_test)
# Per regressió: usar XGBRegressor amb els mateixos paràmetres
Quan usar arbres vs xarxes neuronals?
| Arbres (XGBoost) | Xarxes neuronals | |
|---|---|---|
| Dades tabulars (fulls de càlcul) | Recomanat | Funciona |
| Imatges, àudio, text | No adequat | Recomanat |
| Velocitat d’entrenament | Ràpid | Lent |
| Interpretabilitat | Alta (arbres petits) | Baixa |
| Transfer learning | No | Sí |
| Dataset petit | Funciona bé | Necessita més dades |
Recomanació pràctica
- Dades en taules (preus, classificacions, prediccions numèriques): comença amb XGBoost
- Imatges, vídeo, àudio, text: usa xarxes neuronals
- Si tens dubtes amb dades tabulars, prova ambdós i compara
K-veïns més propers (KNN)
- Introducció
- Aprenentatge basat en instàncies vs basats en models
- Exemple pràctic amb KNN
- Com funciona KNN?
- L’algoritme KNN pas a pas
- Escollir el valor de K
- Mètriques de distància
- Importància de l’escalat de característiques
- KNN per a regressió
- La maledicció de la dimensionalitat
- Avantatges i desavantatges de KNN
- Implementació amb Python
- KNN vs Arbres de Decisió
- Resum
Introducció
Després d’estudiar els arbres de decisió, que construeixen un model explícit a partir de les dades d’entrenament, ara veurem un algoritme radicalment diferent: K-Veïns Més Propers (KNN o K-Nearest Neighbors).
La idea de KNN és molt simple i intuïtiva:
Per classificar un nou exemple, mira quins són els exemples d’entrenament més semblants i assigna-li la mateixa etiqueta que la majoria d’aquests veïns.
Aprenentatge basat en instàncies vs basats en models
Abans de veure com funciona KNN, és important entendre una distinció fonamental en machine learning:
Algoritmes basats en models (Model-based Learning)
Els algoritmes basats en models, com els arbres de decisió o les xarxes neuronals, funcionen en dues fases ben diferenciades:
-
Fase d’entrenament:
- Processen les dades d’entrenament
- Construeixen un model explícit (un arbre, uns pesos, etc.)
- Un cop entrenat, poden descartar les dades d’entrenament
-
Fase de predicció:
- Només utilitzen el model construït
- Són ràpids perquè el model és compacte
Exemple: Un arbre de decisió amb 1000 exemples d’entrenament pot acabar sent un arbre amb només 20 nodes. Per fer una predicció, només cal recórrer l’arbre (molt ràpid).
Algoritmes basats en instàncies (Instance-based Learning)
Els algoritmes basats en instàncies, com KNN, funcionen de manera totalment diferent:
-
Fase d’entrenament:
- Només emmagatzemen les dades
- No construeixen cap model explícit
- També s’anomenen lazy learners (aprenents mandrosos)
-
Fase de predicció:
- Comparen el nou exemple amb TOTES les dades emmagatzemades
- Són lents perquè han de consultar tota la base de dades
Exemple: KNN amb 1000 exemples d’entrenament ha de calcular 1000 distàncies per cada predicció.
Comparació visual
| Aspecte | Model-based (Arbres) | Instance-based (KNN) |
|---|---|---|
| Entrenament | Lent (construeix model) | Instantani (només emmagatzema) |
| Predicció | Ràpid (consulta model) | Lent (cerca en totes les dades) |
| Memòria | Baixa (només el model) | Alta (totes les dades) |
| Analogia | Estudiar i fer un resum | Portar tots els apunts a l’examen |
Exemple pràctic amb KNN
Exemple: Classificació de fruites
Suposem que volem classificar taronges i llimones basant-nos en dues característiques:
- Pes (en grams)
- Grau de dolçor (en una escala de 0 a 10)
Disposem de 10 exemples d’entrenament: 5 taronges i 5 llimones.
Podem visualitzar aquestes dades en un gràfic de dispersió:
- Eix X: Pes (grams)
- Eix Y: Dolçor
- Taronges: punts taronges
- Llimones: punts grocs
Ara arriba una nova fruita amb:
- Pes = 150g
- Dolçor = 6
És una taronja o una llimona?
Com funciona KNN?
L’algoritme KNN segueix aquests passos:
Pas 1: Calcular distàncies
Primer, calculem la distància entre el nou exemple i tots els exemples d’entrenament.
La distància més habitual és la distància euclidiana:
d(x, x’) = \sqrt{(x_1 - x’_1)^2 + (x_2 - x’_2)^2}
On:
- x = (x_1, x_2) és el nou exemple
- x’ = (x’_1, x’_2) és un exemple d’entrenament
Per exemple, si el nou punt té coordenades (150, 6) i un exemple d’entrenament té (158, 6.5):
d = \sqrt{(158-150)^2 + (6.5-6)^2} = \sqrt{64 + 0.25} = \sqrt{64.25} \approx 8.02
Pas 2: Seleccionar els K veïns més propers
Un cop tenim les distàncies a tots els exemples d’entrenament, ordenem els exemples per distància i seleccionem els K més propers.
Per exemple, si K = 3, seleccionem els 3 exemples amb menor distància.
Pas 3: Votació
Finalment, mirem les etiquetes dels K veïns seleccionats i fem una votació majoritària:
- Si la majoria són taronges → predim taronja
- Si la majoria són llimones → predim llimona
Exemple amb K = 3:
- Veí 1: taronja (distància 8.0)
- Veí 2: taronja (distància 10.0)
- Veí 3: llimona (distància 12.0)
Resultat de la votació:
- Taronja: 2 vots
- Llimona: 1 vot
Predicció final: Taronja
L’algoritme KNN pas a pas
Formalment, l’algoritme KNN per a classificació és:
Entrades:
- Conjunt d’entrenament: (x^{(1)}, y^{(1)}), (x^{(2)}, y^{(2)}), \dots, (x^{(m)}, y^{(m)})
- Nou exemple a classificar: x_{\text{nou}}
- Valor de K (nombre de veïns)
Procés:
-
Per cada exemple d’entrenament i = 1, 2, \dots, m:
- Calcula la distància d_i = d(x_{\text{nou}}, x^{(i)})
-
Ordena els exemples d’entrenament per distància creixent
-
Selecciona els K exemples amb menor distància
-
Compte el nombre de cada classe entre aquests K veïns
-
Retorna la classe amb més vots
Sortida:
- Predicció \hat{y} per al nou exemple
Pseudocodi
def knn_classify(X_train, y_train, x_new, K):
# 1. Calcular totes les distàncies
distances = []
for i in range(len(X_train)):
dist = euclidean_distance(x_new, X_train[i])
distances.append((dist, y_train[i]))
# 2. Ordenar per distància
distances.sort(key=lambda x: x[0])
# 3. Seleccionar els K més propers
k_nearest = distances[:K]
# 4. Votació majoritària
votes = {}
for _, label in k_nearest:
votes[label] = votes.get(label, 0) + 1
# 5. Retornar la classe més votada
return max(votes, key=votes.get)
Escollir el valor de K
Una de les decisions més importants en KNN és quant val K.
K massa petit (K=1)
Si K = 1, només mirem el veí més proper. Això pot ser problemàtic:
- Molt sensible al soroll: si hi ha un exemple mal etiquetat, pot donar prediccions incorrectes
- Alta variància: petits canvis en les dades d’entrenament canvien molt les prediccions
- Fronteres de decisió irregulars i fragmentades: cada punt d’entrenament crea la seva pròpia “illa” de decisió

Observa en la gràfica com la frontera de decisió és molt irregular i fragmentada, amb zones petites al voltant de cada punt d’entrenament. Això indica overfitting: el model s’ajusta massa a les dades d’entrenament i no generalitzarà bé.
K massa gran (K=50, tots els punts)
Si K és molt gran (per exemple, igual al nombre total d’exemples):
- Sempre predirà la classe majoritària del conjunt d’entrenament
- Alta bias: el model és massa simple i no captura la complexitat de les dades
- Perd informació local: punts llunyans influeixen en la decisió com si fossin propers
- Frontera de decisió massa suau i lineal

Observa com la frontera és ara molt suau i gairebé lineal, ignorant la distribució real dels punts. Això indica underfitting: el model és massa simple.
K òptim (K=11)
Un valor intermedi de K sol funcionar millor:
- Reducció del soroll: més robust que K=1 perquè diversos veïns voten
- Manté informació local: millor que K massa gran perquè només els veïns propers influeixen
- Frontera equilibrada: prou suau per generalitzar, però prou flexible per capturar la forma real de les dades

Observa com la frontera és més suau que amb K=1 però encara respecta la distribució dels punts, creant una separació natural entre taronges i llimones. Aquest és el compromís òptim entre bias i variància.
Recomanacions pràctiques:
- Començar amb K = \sqrt{m}, on m és el nombre d’exemples d’entrenament
- Utilitzar validació creuada per trobar el millor K
- Preferir valors imparells de K per evitar empats en classificació binària
Trobar K amb validació creuada
El procés típic és:
- Provar diferents valors de K: per exemple, K = 1, 3, 5, 7, 9, 11, \dots
- Per a cada valor de K:
- Entrenar el model amb el conjunt d’entrenament
- Avaluar-lo amb el conjunt de validació
- Calcular l’error de validació
- Escollir el K amb menor error de validació
- Avaluar el model final amb el conjunt de test
La gràfica mostra com l’error de validació varia amb K:
- Per K petit: overfitting (alta variància)
- Per K gran: underfitting (alta bias)
- El mínim indica el millor valor de K
Mètriques de distància
La distància euclidiana no és l’única opció. Depenent del problema, altres mètriques poden funcionar millor.
Distància euclidiana
És la distància “en línia recta” en l’espai euclidià:
d_{\text{euclidiana}}(x, x’) = \sqrt{\sum_{i=1}^{n} (x_i - x’_i)^2}
- Avantatge: intuïtiva, funciona bé en molts casos
- Desavantatge: sensible a l’escala de les característiques
Distància de Manhattan
També anomenada distància L1 o city-block distance:
d_{\text{Manhattan}}(x, x’) = \sum_{i=1}^{n} |x_i - x’_i|
Mesura la distància com si es caminés per carrers en una quadrícula.
- Avantatge: menys sensible a outliers que l’euclidiana
- Ús: funciona bé en espais d’alta dimensió
Distància de Minkowski
Una generalització de les anteriors:
d_{\text{Minkowski}}(x, x’) = \left(\sum_{i=1}^{n} |x_i - x’_i|^p\right)^{1/p}
On:
- p = 1: distància de Manhattan
- p = 2: distància euclidiana
- p = \infty: distància de Chebyshev (\max_i |x_i - x’_i|)
Comparació visual
Quan usar cada mètrica?
| Mètrica | Millor per a |
|---|---|
| Euclidiana | Dades contínues, característiques amb unitats similars |
| Manhattan | Dades d’alta dimensió, presència d’outliers |
| Cosine | Dades de text (vectors de paraules), direcció més important que magnitud |
Importància de l’escalat de característiques
Un problema important de KNN és que és molt sensible a l’escala de les característiques.
Exemple problemàtic
Imaginem que volem classificar cases en “cares” o “barates” basant-nos en:
- Característica 1: Superfície (en m²) → rang [50, 300]
- Característica 2: Nombre d’habitacions → rang [1, 5]
Si calculem la distància euclidiana sense normalitzar:
d = \sqrt{(200 - 150)^2 + (3 - 2)^2} = \sqrt{2500 + 1} = \sqrt{2501} \approx 50
La característica de superfície domina el càlcul perquè els seus valors són molt més grans!
Solució: Normalització
Hem d’escalar totes les característiques a un rang similar.
Min-Max Scaling
Escala els valors a l’interval [0, 1]:
x’_i = \frac{x_i - \min(x)}{\max(x) - \min(x)}
Standardització (Z-score)
Transforma les dades per tenir mitjana 0 i desviació estàndard 1:
x’_i = \frac{x_i - \mu}{\sigma}
On:
- \mu és la mitjana
- \sigma és la desviació estàndard
Exemple pràctic
Abans de normalitzar:
| Casa | Superfície (m²) | Habitacions | Preu |
|---|---|---|---|
| 1 | 100 | 2 | Barat |
| 2 | 200 | 4 | Car |
Després de min-max scaling:
| Casa | Superfície | Habitacions | Preu |
|---|---|---|---|
| 1 | 0.0 | 0.0 | Barat |
| 2 | 1.0 | 1.0 | Car |
Ara ambdues característiques tenen el mateix pes en el càlcul de distàncies.
KNN per a regressió
KNN no només serveix per a classificació. També podem utilitzar-lo per regressió, és a dir, per predir valors numèrics.
Diferència clau
En lloc de fer una votació majoritària, calculem la mitjana dels valors dels K veïns més propers.
Algoritme KNN per regressió
Procés:
- Calcular distàncies a tots els exemples d’entrenament
- Seleccionar els K veïns més propers
- Calcular la mitjana dels valors y d’aquests veïns
- Retornar aquesta mitjana com a predicció
Formalment:
\hat{y} = \frac{1}{K} \sum_{i \in \text{K-veïns}} y^{(i)}
Exemple: Predir el preu d’un habitatge
Suposem que volem predir el preu d’un habitatge basant-nos en la seva superfície.
Conjunt d’entrenament:
| Superfície (m²) | Preu (k€) |
|---|---|
| 50 | 100 |
| 80 | 150 |
| 100 | 180 |
| 120 | 200 |
| 150 | 250 |
Nova casa: 90 m²
Amb K = 3, els 3 veïns més propers són:
| Superfície | Preu | Distància |
|---|---|---|
| 80 | 150 | 10 |
| 100 | 180 | 10 |
| 50 | 100 | 40 |
Predicció:
\hat{y} = \frac{150 + 180 + 100}{3} = \frac{430}{3} \approx 143.3 \text{ k€}
Variació: Mitjana ponderada
En lloc d’una mitjana simple, podem donar més pes als veïns més propers:
\hat{y} = \frac{\sum_{i \in \text{K-veïns}} w_i \cdot y^{(i)}}{\sum_{i \in \text{K-veïns}} w_i}
On el pes pot ser:
w_i = \frac{1}{d_i}
(inversament proporcional a la distància)
Exemples més propers tenen més influència en la predicció.
La maledicció de la dimensionalitat
Un dels problemes més importants de KNN és la maledicció de la dimensionalitat (curse of dimensionality).
Què és?
Quan el nombre de característiques (dimensions) augmenta, passa alguna cosa sorprenent:
Tots els punts es tornen aproximadament equidistants entre si.
Això fa que el concepte de “veí més proper” perdi significat.
Exemple intuïtiu
Imaginem que tenim punts distribuïts aleatòriament en un hiperespai:
- 1D (una línia): És fàcil trobar punts propers
- 2D (un pla): Encara funciona bé
- 3D (un cub): Comença a complicar-se
- 100D: Tots els punts estan aproximadament a la mateixa distància!
Per què passa?
En dimensions altes:
- El volum de l’espai creix exponencialment
- Els punts es dispersen molt
- La distància entre el veí més proper i el més llunyà convergeix
Matemàticament, la ràtio:
\frac{d_{\max} - d_{\min}}{d_{\min}} \to 0 \quad \text{quan } n \to \infty
Conseqüències per KNN
- Necessitem moltes més dades en dimensions altes
- Les prediccions es tornen menys fiables
- El temps de càlcul augmenta (hem de calcular distàncies en més dimensions)
Solucions
-
Reducció de dimensionalitat:
- PCA (Principal Component Analysis)
- Feature selection (seleccionar només les característiques més rellevants)
-
Regularització: Penalitzar característiques poc informatives
-
Usar altres algoritmes: En dimensions molt altes, arbres de decisió o xarxes neuronals poden funcionar millor
Avantatges i desavantatges de KNN
Avantatges
-
Molt simple d’entendre i implementar
- No requereix matemàtiques avançades
- Codi molt curt
-
No necessita entrenament
- És un algoritme “lazy” (mandrós)
- Només emmagatzema les dades
-
Funciona bé amb dades no lineals
- No assumeix cap distribució de les dades
- Fronteres de decisió flexibles
-
Pot fer tant classificació com regressió
- Mateix algoritme, només canvia l’agregació final
-
Adaptable a nous exemples
- Només cal afegir nous punts al conjunt d’entrenament
Desavantatges
-
Computacionalment costós per a prediccions
- Cada predicció costa O(m \cdot n)
- On m = nombre d’exemples, n = nombre de dimensions
- Per grans datasets, és molt lent
-
Requereix molta memòria
- Ha d’emmagatzemar tot el conjunt d’entrenament
- Problemàtic amb datasets grans
-
Sensible a l’escala de les característiques
- Sempre cal normalitzar les dades
-
Sensible al soroll i outliers
- Un exemple mal etiquetat pot afectar les prediccions
-
Maledicció de la dimensionalitat
- No funciona bé amb moltes característiques
- Necessita moltes dades en dimensions altes
-
Difícil amb característiques categòriques
- Necessita mètriques de distància adaptades
Implementació amb Python
A continuació veurem com implementar KNN amb la biblioteca scikit-learn.
Classificació amb KNN
from sklearn.neighbors import KNeighborsClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, classification_report
# 1. Preparar les dades
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# 2. Normalitzar les característiques (IMPORTANT!)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
# 3. Crear i entrenar el model KNN
knn = KNeighborsClassifier(n_neighbors=5)
knn.fit(X_train_scaled, y_train)
# 4. Fer prediccions
y_pred = knn.predict(X_test_scaled)
# 5. Avaluar
accuracy = accuracy_score(y_test, y_pred)
print(f"Accuracy: {accuracy:.2%}")
print("\nClassification Report:")
print(classification_report(y_test, y_pred))
Regressió amb KNN
from sklearn.neighbors import KNeighborsRegressor
from sklearn.metrics import mean_squared_error, r2_score, mean_absolute_error
# Crear i entrenar el model per regressió
knn_reg = KNeighborsRegressor(n_neighbors=5)
knn_reg.fit(X_train_scaled, y_train)
# Fer prediccions
y_pred = knn_reg.predict(X_test_scaled)
# Avaluar amb múltiples mètriques
mse = mean_squared_error(y_test, y_pred)
rmse = mean_squared_error(y_test, y_pred, squared=False)
mae = mean_absolute_error(y_test, y_pred)
r2 = r2_score(y_test, y_pred)
print(f"MSE: {mse:.2f}")
print(f"RMSE: {rmse:.2f}")
print(f"MAE: {mae:.2f}")
print(f"R²: {r2:.2f}")
Trobar el millor K
from sklearn.model_selection import GridSearchCV
# Definir el rang de valors de K a provar
param_grid = {
'n_neighbors': [1, 3, 5, 7, 9, 11, 15, 21],
'weights': ['uniform', 'distance'],
'metric': ['euclidean', 'manhattan']
}
# Crear el model base
knn = KNeighborsClassifier()
# Cerca exhaustiva amb validació creuada (cv=5 significa k=5 folds)
grid_search = GridSearchCV(
knn, param_grid, cv=5, scoring='accuracy', n_jobs=-1, verbose=1
)
grid_search.fit(X_train_scaled, y_train)
# Millors hiperparàmetres
print(f"Millors hiperparàmetres: {grid_search.best_params_}")
print(f"Millor accuracy (CV): {grid_search.best_score_:.2%}")
# Usar el millor model
best_knn = grid_search.best_estimator_
y_pred = best_knn.predict(X_test_scaled)
test_accuracy = accuracy_score(y_test, y_pred)
print(f"Accuracy en test: {test_accuracy:.2%}")
Opcions addicionals
# Usar distància de Manhattan
knn = KNeighborsClassifier(n_neighbors=5, metric='manhattan')
# Usar pesos basats en distància
knn = KNeighborsClassifier(n_neighbors=5, weights='distance')
# Especificar l'algoritme de cerca de veïns
knn = KNeighborsClassifier(n_neighbors=5, algorithm='ball_tree')
KNN vs Arbres de Decisió
Ara que hem vist tant KNN com arbres de decisió, fem una comparació:
| Aspecte | KNN | Arbres de Decisió |
|---|---|---|
| Tipus | Instance-based (lazy) | Model-based |
| Entrenament | Instant (només emmagatzema) | Pot trigar (construeix model) |
| Predicció | Lent (O(m \cdot n)) | Ràpid (O(\log m)) |
| Memòria | Alta (tot el dataset) | Baixa (només l’arbre) |
| Interpretabilitat | Difícil | Fàcil (arbres petits) |
| Fronteres de decisió | Suaus, no lineals | Rectangulars, ortogonals |
| Normalització | Imprescindible | No necessària |
| Alta dimensionalitat | Problemàtic | Funciona bé |
| Soroll | Sensible | Menys sensible (amb ensembles) |
| Característiques categòriques | Requereix codificació especial | Maneja directament |
Quan usar KNN?
- Datasets petits o mitjans (< 10,000 exemples)
- Poques dimensions (< 20 característiques)
- Dades amb fronteres de decisió complexes
- Quan no cal interpretabilitat
- Quan el temps de predicció no és crític
Quan usar arbres de decisió?
- Datasets grans
- Moltes dimensions
- Quan necessitem prediccions ràpides
- Quan volem interpretabilitat
- Dades tabulars amb característiques mixtes (categòriques + contínues)
- Quan hi ha soroll (usar ensembles com Random Forest o XGBoost)
Resum
-
KNN és un algoritme d’aprenentatge instance-based que classifica nous exemples basant-se en la similitud amb exemples d’entrenament.
-
El valor de K és crític:
- K petit → alta variància, sensible al soroll
- K gran → alta bias, perd informació local
- Trobar amb validació creuada
-
Distàncies: Euclidiana, Manhattan, Minkowski, etc.
- La tria depèn del problema
-
Escalat de característiques és imprescindible per evitar que unes característiques dominin sobre altres.
-
Avantatges: Simple, flexible, funciona bé amb fronteres no lineals.
-
Desavantatges: Lent en predicció, requereix molta memòria, maledicció de la dimensionalitat.
-
Comparació amb arbres de decisió: KNN és millor per datasets petits amb poques dimensions, mentre que arbres són millors per datasets grans amb moltes dimensions.
KNN és un algoritme fonamental en machine learning que, tot i la seva simplicitat, és sorprenentment efectiu en moltes aplicacions pràctiques. La seva comprensió ajuda a entendre conceptes importants com la importància de l’escalat de dades i els reptes de l’alta dimensionalitat.
Aprenentatge No Supervisat
- Tipologia
- Agrupament (clustering)
- Agrupació amb K-means
- Detecció d’anomalies (Anomaly Detection)
- Anàlisi de components principals
- Errors comuns a evitar
- Apèndix: Detecció d’anomalies (ampliació)
Imagina que tens milions de clients però cap etiqueta que et digui qui són. O que gestiones una fàbrica i vols detectar peces defectuoses sense haver vist mai abans cap defecte. O que vols visualitzar dades amb 50 variables en un simple gràfic 2D.
L’aprenentatge no supervisat resol aquests problemes: descobreix patrons en dades sense necessitat d’etiquetes prèvies. Mentre que l’aprenentatge supervisat aprèn d’exemples etiquetats (spam/no spam, gat/gos), l’aprenentatge no supervisat explora les dades per trobar estructures ocultes pel seu compte.
Tipologia
L’aprenentatge no supervisat treballa amb dades sense etiquetes, buscant patrons o estructures ocultes en les dades.
Aquests són alguns tipus d’aprenentatge no supervisat:
-
Clustering (Agrupació)
- Objectiu: Agrupar dades en dos o més grups segons similituds
- Exemple: Segmentar clients segons el seu comportament de compra
-
Detecció d’anomalies
- Objectiu: Identificar esdeveniments o transaccions inusuals
- Exemple: Detectar frau en transaccions bancàries
-
Reducció de dimensionalitat
- Objectiu: Comprimir dades utilitzant menys variables
- Exemple: Reduir la complexitat d’imatges mantenint la informació essencial
Agrupament (clustering)
Un algorisme d’agrupament analitza un conjunt de punts de dades i troba automàticament aquells que són semblants o relacionats entre si.
Vegem què significa això, tot contrastant l’agrupament —que és un mètode d’aprenentatge no supervisat— amb l’aprenentatge supervisat que ja havíem vist en la classificació binària.
Supervisat vs. No supervisat
Imaginem un conjunt de dades amb dues característiques, x_1 i x_2.
-
Aprenentatge supervisat: Disposem d’un conjunt d’entrenament amb les entrades x i les etiquetes corresponents y. Podem representar aquestes dades i ajustar-hi, per exemple, una regressió logística o una xarxa neuronal que aprengui una frontera de decisió entre les classes.
En aquest cas, el conjunt de dades conté (x, y):
-
Aprenentatge no supervisat:
Només tenim les entrades x, però no les etiquetes y.Quan representem el conjunt de dades, només veiem punts (●), sense colors ni símbols que ens indiquin quina classe correspon a cada punt.
Com que no tenim les etiquetes y, no podem dir a l’algorisme quina és la resposta correcta. En comptes d’això, demanem a l’algorisme que trobi estructura interessant en les dades.
L’algorisme pot identificar grups de punts similars i calcular el seu centroide: el punt central que representa la posició mitjana de tots els punts d’un grup.
Agrupament: trobar estructura
El primer algorisme d’aprenentatge no supervisat que s’acostuma a estudiar és l’agrupament (clustering).
Aquest mètode busca si el conjunt de dades pot ser dividit en grups (clústers) de punts que siguin semblants entre si.
Per exemple, un algorisme d’agrupament podria descobrir que les dades provenen de dos clústers diferenciats.
Aplicacions de l’agrupament
L’agrupament és àmpliament utilitzat en camps molt diversos:
-
Processament de textos i notícies:
Agrupar articles similars per temàtica (p. ex. notícies sobre pandes o sobre mercats financers). -
Segmentació de mercat:
Classificar els usuaris en grups segons objectius comuns: millorar competències, desenvolupar carrera professional, o estar al dia en IA. -
Biologia i genètica:
Analitzar dades d’expressió genètica i agrupar persones amb trets similars. -
Astronomia:
Agrupar cossos celestes per identificar galàxies o estructures coherents a l’espai.
En definitiva, l’agrupament ajuda a descobrir patrons ocults en dades sense necessitat de tenir les etiquetes.
Agrupació amb K-means
K-means és el mètode d’agrupament molt utilitzat. Explorarem com funciona i per què és tan comú en aplicacions pràctiques.
Suposem que tenim un conjunt de dades amb 30 exemples d’entrenament no etiquetats (30 punts en un gràfic). El que volem fer és executar K-means sobre aquest conjunt de dades.
El primer que fa K-means és escollir aleatòriament la posició inicial dels centroides dels clústers.
En aquest exemple demanarem a l’algorisme que trobi dos clústers.
Per tant, inicialment col·loca dues creus (una vermella i una blava) en posicions qualsevol.
Aquestes primeres posicions són només suposicions inicials i no tenen per què ser bones. Però serveixen com a punt de partida.
Una idea fonamental és que K-means repeteix dues operacions fins a convergir:
-
Assignació de punts als centroides
Per a cada punt del conjunt de dades, l’algorisme comprova si està més a prop del centroide vermell o del centroide blau i l’assigna al clúster corresponent.
-
Actualització dels centroides
Un cop cada punt està assignat a un clúster, K-means calcula la mitjana de les posicions dels punts de cada clúster:
- El centroide vermell es mou a la posició mitjana de tots els punts vermells.
- El centroide blau es mou a la posició mitjana de tots els punts blaus.
D’aquesta manera, els centroides es recol·loquen en posicions més representatives.
Aquest procés es repeteix fins que els punts ja no canvien de clúster i els centroides deixen de moure’s. Quan això passa, diem que K-means ha convergit.
Algorisme K-means
L’algorisme K-means es pot resumir així:
-
Inicialització: Col·locar aleatòriament K centroides (\mu_1, \mu_2, \dots, \mu_K). Cada centroide té la mateixa dimensió que les dades.
-
Repetir fins a convergència:
- Assignació: Cada punt s’assigna al centroide més proper (usant distància euclidiana).
- Actualització: Cada centroide es mou a la mitjana dels punts que té assignats.
Exemple numèric pas a pas
Dades: 4 punts en 2D amb K=2 clústers: A=(1,1), B=(2,1), C=(4,3), D=(5,4).
Inicialització: Escollim aleatòriament \mu_1=(1,1) i \mu_2=(2,1) com a centroides inicials.
Iteració 1:
Assignació — Calculem la distància euclidiana de cada punt als dos centroides:
| Punt | Dist. a \mu_1 | Dist. a \mu_2 | Assignació |
|---|---|---|---|
| A(1,1) | 0 | 1 | Clúster 1 |
| B(2,1) | 1 | 0 | Clúster 2 |
| C(4,3) | 3.6 | 2.8 | Clúster 2 |
| D(5,4) | 5.0 | 4.2 | Clúster 2 |
Actualització — Recalculem els centroides com la mitjana dels punts assignats:
- \mu_1 = A = (1, 1)
- \mu_2 = \frac{B + C + D}{3} = \frac{(2,1) + (4,3) + (5,4)}{3} = (3.67, 2.67)
Iteració 2:
Assignació — Amb els nous centroides \mu_1=(1,1) i \mu_2=(3.67, 2.67):
| Punt | Dist. a \mu_1 | Dist. a \mu_2 | Assignació |
|---|---|---|---|
| A(1,1) | 0 | 3.1 | Clúster 1 |
| B(2,1) | 1 | 2.4 | Clúster 1 |
| C(4,3) | 3.6 | 0.5 | Clúster 2 |
| D(5,4) | 5.0 | 1.9 | Clúster 2 |
Actualització — Recalculem els centroides:
- \mu_1 = \frac{A + B}{2} = \frac{(1,1) + (2,1)}{2} = (1.5, 1)
- \mu_2 = \frac{C + D}{2} = \frac{(4,3) + (5,4)}{2} = (4.5, 3.5)
Convergència: Si executem una tercera iteració, els punts no canvien de clúster. L’algorisme ha convergit amb resultat: {A, B} i {C, D}.
Casos especials
Si un cluster no té cap punt assignat, la mitjana no està definida.
Les opcions habituals són:
- Eliminar el cluster i treballar amb K-1 clusters.
- Reinicialitzar aleatòriament el centroide i esperar que rebi punts en la següent iteració.
Aplicació pràctica
K-means s’aplica sovint a dades que no estan clarament separades.
Exemple: disseny de samarretes petites, mitjanes i grans segons alçada i pes dels clients.
- Els punts poden variar contínuament sense clústers evidents.
- K-means amb K = 3 pot agrupar els punts en tres clústers representatius.
- Els centroides resultants indiquen les mesures més representatives per a cada talla.
K-means intenta optimitzar una funció de cost específica, i aquesta és la raó per la qual convergeix després de diverses iteracions.
Funció de Cost
K-means optimitza una funció de cost anomenada funció de distorsió:
J = \frac{1}{m} \sum_{i=1}^{m} | x_i - \mu_{C_i} |^2
Aquesta fórmula mesura la mitjana de la distància al quadrat entre cada punt i el centroide del seu clúster. Com més petita sigui J, més compactes són els clústers.
Per què funciona K-means?
- El pas d’assignació minimitza J respecte a les assignacions (cada punt va al centroide més proper).
- El pas d’actualització minimitza J respecte a les posicions dels centroides (la mitjana minimitza la suma de distàncies al quadrat).
Com que cada pas redueix o manté J, l’algoritme sempre convergeix.
Inicialització i mínims locals
Inicialització de centroides
El primer pas de K-means és escollir ubicacions inicials \mu_1, \dots, \mu_K.
La manera més comuna és seleccionar aleatòriament (K) exemples del training set.
Important: K \le m, on m és el nombre d’exemples.
Problema de mínims locals
Diferents inicialitzacions poden produir clústers molt diferents, atrapats en mínims locals de la funció de cost J. La solució és executar múltiples inicialitzacions:
- Repetir K-means N vegades amb inicialitzacions aleatòries.
- Calcular la funció de cost J per cada execució.
- Seleccionar els centroides amb el menor cost.
Normalment N \in [50, 1000]. Això millora l’agrupament i minimitza la distorsió.
Resumint:
- Escollir centroides inicials és crucial.
- Múltiples inicialitzacions redueixen el risc de mínims locals.
- Escollir el conjunt amb menor distorsió ((J)) dona un millor resultat final.
Selecció de K
L’algorisme K-means requereix com a entrada un paràmetre K, el nombre de clusters que volem identificar. Decidir el valor correcte de K és sovint ambigu, ja que diferents observadors poden interpretar les mateixes dades de manera diferent.
Mètode del colze
Una tècnica habitual és el mètode del colze:
- Executar K-means amb diversos valors de K (per exemple, de 1 a 10).
- Plotar la inèrcia J (eix Y) en funció de K (eix X).
A mesura que K augmenta, J disminueix (més centroides → punts més a prop del seu centroide). El colze és el punt on la corba deixa de baixar ràpidament i s’aplana. Minimitzar J directament no funciona, ja que J disminueix amb més clusters.
Selecció pràctica de K
La millor manera de triar K és segons l’ús final dels clusters.
Exemples:
- Talles de samarretes: K=3 → S, M, L; K=5 → XS, S, M, L, XL. La decisió depèn del trade-off entre ajust i costos de producció.
- Compressió d’imatges: K determina qualitat vs espai ocupat.
Resumint:
- Escollir K depèn del propòsit i no sempre té una resposta única.
- El mètode del colze és útil però no infal·lible.
- Comparar diferents K segons la utilitat dels clusters és la millor estratègia.
Implementació amb scikit-learn
from sklearn.cluster import KMeans
# Entrenar el model
kmeans = KMeans(n_clusters=3, random_state=42, n_init=10)
kmeans.fit(X)
# Resultats
labels = kmeans.labels_ # Assignació de cada punt
centers = kmeans.cluster_centers_ # Posició dels centroides
inertia = kmeans.inertia_ # Valor de J (funció de cost), també anomenat "inèrcia"
# Predir clúster per a noves dades
new_labels = kmeans.predict(X_new)
Per què “inèrcia”? El terme prové de la física: el moment d’inèrcia mesura com la massa es distribueix al voltant d’un eix de rotació. De manera anàloga, la inèrcia de K-means mesura com els punts es distribueixen al voltant dels seus centroides. Com més compactes són els clústers, menor és la inèrcia.
Paràmetres principals:
n_clusters: El nombre de clústers K que volem trobar.n_init: Nombre d’inicialitzacions aleatòries. L’algorisme s’executan_initvegades amb diferents centroides inicials i retorna el millor resultat (el que té menor J). Valors típics: 10-100 per problemes petits, fins a 1000 per problemes crítics.random_state: Llavor per reproduïbilitat dels resultats.
Resum K-means: Agrupa dades en K clústers basant-se en distàncies. Itera entre assignar punts i moure centroides fins convergir. Executa múltiples vegades amb inicialitzacions diferents. Utilitza el mètode del colze o el context del problema per escollir K.
Detecció d’anomalies (Anomaly Detection)
La detecció d’anomalies és una tècnica d’aprenentatge automàtic no supervisat que permet identificar observacions que difereixen significativament del comportament normal esperat. A diferència dels algoritmes supervisats, no necessitem grans quantitats d’exemples anòmals etiquetats per entrenar el model.
Per què és important?
En moltes aplicacions del món real, les anomalies són rares però crítiques:
- Fabricació de motors d’avió: Un motor defectuós pot tenir conseqüències catastròfiques. Necessitem detectar qualsevol comportament anòmal abans que l’avió voli.
- Detecció de frau financer: Les transaccions fraudulentes són poc freqüents (menys del 0.1% en targetes de crèdit), però cada frau no detectat representa pèrdues econòmiques.
- Monitoratge de servidors: Un servidor compromès o amb una fallada pot mostrar patrons de comportament inusuals en memòria, CPU o xarxa.
En tots aquests casos, tenim moltes dades de funcionament normal, però molt poques (o cap) mostra d’anomalies. Això fa que els mètodes tradicionals supervisats no siguin aplicables.
Aplicacions habituals
| Àmbit | Característiques analitzades | Objectiu |
|---|---|---|
| Frau financer | Import, ubicació, freqüència, hora, tipus de producte | Detectar transaccions sospitoses |
| Fabricació | Temperatura, vibracions, pressió, toleràncies dimensionals | Identificar unitats defectuoses |
| Ciberseguretat | Tràfic de xarxa, inicis de sessió, accés a fitxers | Detectar intrusions o comportaments maliciosos |
| Salut | Constants vitals, patrons d’activitat, valors analítiques | Alertar de situacions d’emergència |
Isolation Forest
Isolation Forest és l’algoritme més utilitzat actualment per a detecció d’anomalies en aplicacions reals. La seva idea central és molt intuïtiva: les anomalies són més fàcils d’aïllar que les observacions normals.
Imaginem que tenim un conjunt de punts en un espai 2D. La majoria estan agrupats en una regió densa, però alguns punts estan molt allunyats d’aquest cluster principal.
Si volem “aïllar” un punt normal (dins del cluster), necessitarem fer moltes divisions per separar-lo de la resta. En canvi, si volem aïllar un punt anòmal (lluny del cluster), amb poques divisions ja l’haurem separat completament.
Aquesta és exactament la lògica que utilitza Isolation Forest:
- Construeix múltiples arbres de decisió aleatoris
- Cada arbre intenta aïllar punts mitjançant divisions aleatòries
- Mesura la longitud del camí necessària per aïllar cada punt
- Camins curts → probable anomalia
- Camins llargs → probable observació normal
Com funciona l’algoritme
Isolation Forest crea un “bosc” d’arbres d’aïllament. Cada arbre es construeix així:
- Selecciona aleatòriament una característica x_j del conjunt de dades
- Selecciona aleatòriament un valor de tall entre el mínim i el màxim de x_j
- Divideix les dades en dos grups segons aquest tall
- Repeteix recursivament per a cada subgrup fins que cada punt quedi aïllat
Exemple visual (arbre simplified):
[200 punts]
|
x1 < 5.2? (tall aleatori)
/ \
[180 punts] [20 punts] ← Grup petit (sospitós!)
| |
x2 < 3.1? x2 < 8.5?
/ \ / \
[120] [60] [18] [2] ← Aïllat ràpidament!
| | | |
... ... ... ANOMALIA
Un punt anòmal queda aïllat amb menys divisions (camí curt a l’arbre). Un punt normal requereix més divisions (camí llarg).
Implementació amb scikit-learn
from sklearn.ensemble import IsolationForest
import numpy as np
# Generar dades sintètiques per demostració
np.random.seed(42)
X_normal = np.random.randn(200, 2) # 200 punts normals
X_anomalies = np.random.uniform(low=-4, high=4, size=(10, 2)) # 10 anomalies
X = np.vstack([X_normal, X_anomalies])
# Entrenar Isolation Forest
clf = IsolationForest(
contamination=0.05, # Esperem ~5% d'anomalies
random_state=42,
n_estimators=100 # Nombre d'arbres al bosc
)
clf.fit(X)
# Predir: 1 = normal, -1 = anomalia
predictions = clf.predict(X)
anomalies = X[predictions == -1]
print(f"Anomalies detectades: {len(anomalies)}")
El paràmetre de contaminació
El paràmetre més important d’Isolation Forest és contamination, que indica la proporció esperada d’anomalies al conjunt de dades.
- Valor per defecte: 0.1 (10%)
- Valor típic: Entre 0.01 i 0.1, depenent del problema
- Com ajustar-lo: Utilitzar un conjunt de validació amb algunes anomalies etiquetades (veure Avaluació i divisió de dades)
Trade-off important:
- Contamination massa alt → Molts falsos positius (etiquetem normals com anòmals)
- Contamination massa baix → Molts falsos negatius (perdem anomalies reals)
Scores d’anomalia
A més de les prediccions binàries (-1 o 1), Isolation Forest proporciona un score continu per cada observació:
# Obtenir scores d'anomalia
scores = clf.decision_function(X)
# Valors més negatius = més anòmals
# Valors més positius = més normals
# Ordenar per score per veure els casos més sospitosos
import pandas as pd
df_scores = pd.DataFrame({
'score': scores,
'prediction': predictions
})
df_sorted = df_scores.sort_values('score')
print(df_sorted.head(10)) # 10 casos més anòmals
Aquest score és útil per:
- Prioritzar casos per revisió manual
- Establir llindars personalitzats segons el context de negoci
- Visualitzar la distribució d’anomalies detectades
Avantatges i limitacions
| Avantatges | Limitacions |
|---|---|
| No assumeix cap distribució específica de les dades | Cal ajustar el paràmetre contamination |
| Funciona bé en alta dimensionalitat (moltes features) | Pot tenir problemes si hi ha múltiples clusters normals |
| Computacionalment eficient fins i tot amb grans datasets | No proporciona probabilitats explícites com models generatius |
| No requereix normalització de dades | Sensible a característiques irrellevants (cal feature engineering) |
Avaluació i divisió de dades
Un dels aspectes més delicats de la detecció d’anomalies és com avaluar el model. Tot i que és una tècnica no supervisada, és molt útil disposar d’algunes anomalies etiquetades per poder mesurar el rendiment.
La divisió de dades en detecció d’anomalies és diferent de l’aprenentatge supervisat clàssic:
Regla clau: El conjunt d’entrenament ha de contenir només exemples normals (o una proporció molt alta de normals). Si s’escapa alguna anomalia al conjunt d’entrenament, normalment no passa res. L’algoritme és robust a una petita contaminació.
Però quantes normals usar per entrenar? Això depèn del context. Hi ha dues estratègies principals:
Estratègia 1: Split 60/20/20 (ajust d’hiperparàmetres)
Quan usar-la:
- Necessites ajustar el paràmetre
contaminationexperimentalment - Tens un nombre limitat d’exemples normals (milers, no centenars de milers)
- Vols seguir les millors pràctiques de ML supervisat (separar train/val/test)
- Pots permetre’t “perdre” dades d’entrenament per tenir validació
Exemple pràctic amb motors d’avió:
Suposem que una fàbrica ha recollit dades de 10.000 motors durant anys:
- 10.000 motors normals (funcionen correctament)
- 20 motors defectuosos (anomalies detectades posteriorment)
Divisió recomanada:
Entrenament: 6.000 motors normals (60%, només y=0)
Validació: 2.000 motors normals + 10 defectuosos (20% + 50% anomalies)
Test: 2.000 motors normals + 10 defectuosos (20% + 50% anomalies)
Si tenim molt poques anomalies (menys de 20), podem prescindir del conjunt de test:
Entrenament: 6.000 motors normals
Validació: 4.000 motors normals + 20 defectuosos
Implementació de la divisió 60/20/20:
import numpy as np
# Assumim que tenim 'X' (característiques) i 'y' (0=normal, 1=anomalia)
# Separar normals i anomalies
normal_indices = np.where(y == 0)[0]
anomaly_indices = np.where(y == 1)[0]
# Entrenament: 60% dels normals
n_train = int(0.6 * len(normal_indices))
train_idx = normal_indices[:n_train]
X_train = X[train_idx]
# Validació: 20% normals + 50% anomalies
val_normal_idx = normal_indices[n_train:n_train + int(0.2*len(normal_indices))]
val_anomaly_idx = anomaly_indices[:len(anomaly_indices)//2]
val_idx = np.concatenate([val_normal_idx, val_anomaly_idx])
X_val = X[val_idx]
y_val = y[val_idx]
# Test: Resta de dades
test_normal_idx = normal_indices[n_train + int(0.2*len(normal_indices)):]
test_anomaly_idx = anomaly_indices[len(anomaly_indices)//2:]
test_idx = np.concatenate([test_normal_idx, test_anomaly_idx])
X_test = X[test_idx]
y_test = y[test_idx]
print(f"Train: {len(X_train)} normals")
print(f"Val: {len(X_val)} total ({sum(y_val==1)} anomalies)")
print(f"Test: {len(X_test)} total ({sum(y_test==1)} anomalies)")
Ajust del paràmetre contamination amb validació:
from sklearn.metrics import f1_score
resultats = []
for c in [0.01, 0.02, 0.05, 0.1, 0.15]:
clf = IsolationForest(contamination=c, random_state=42)
clf.fit(X_train) # Només normals!
y_pred = clf.predict(X_val)
y_pred_binary = (y_pred == -1).astype(int)
f1 = f1_score(y_val, y_pred_binary)
resultats.append({'contamination': c, 'f1_score': f1})
print(f"Contaminació={c:.2f}, F1-score={f1:.3f}")
# Seleccionar el valor amb millor F1-score
best_c = max(resultats, key=lambda x: x['f1_score'])['contamination']
print(f"\nMillor contamination: {best_c}")
Estratègia 2: Usar TOTES les normals (producció)
Quan usar-la:
- Tens moltes dades normals (centenars de milers o milions)
- Les anomalies són molt rares (< 0.5% del total)
- El paràmetre
contaminationes pot fixar segons coneixement del domini - Vols maximitzar l’aprenentatge del comportament normal
- No necessites ajustar hiperparàmetres experimentalment
Exemple pràctic: Detecció de frau bancari:
- 284.315 transaccions normals (abundants!)
- 492 transaccions fraudulentes (0.173%, molt rares)
- El
contaminationes fixa a 0.002 segons estadístiques del sector
Divisió recomanada:
Entrenament: 284.315 normals (100% de normals disponibles)
Test: 284.807 total (totes les dades, incloent 492 fraus)
Per què usar TOTES les normals?
- Dades normals abundants: No cal “estalviar-ne” per validació
- Anomalies precioses: Amb només 492 fraus, dividir-les redueix el poder estadístic del test
- Contamination fix: El sector bancari sap que ~0.2% de transaccions són frau
- Maximitza aprenentatge: Més exemples normals → millor model del “comportament normal”
Implementació 100%/100%:
import numpy as np
# Separar normals i anomalies
normal_indices = np.where(y == 0)[0]
X_train = X[normal_indices] # TOTES les normals
# Test: totes les dades (manté desbalanceig natural)
X_test = X
y_test = y
print(f"Train: {len(X_train)} normals (100%)")
print(f"Test: {len(X_test)} total ({sum(y_test==1)} anomalies, {100*sum(y_test==1)/len(y_test):.3f}%)")
# Entrenar amb totes les normals
clf = IsolationForest(contamination=0.002, random_state=42, n_estimators=100)
clf.fit(X_train)
# Avaluar directament sobre totes les dades
y_pred = clf.predict(X_test)
y_pred_binary = (y_pred == -1).astype(int)
from sklearn.metrics import precision_score, recall_score, f1_score
precision = precision_score(y_test, y_pred_binary, zero_division=0)
recall = recall_score(y_test, y_pred_binary)
f1 = f1_score(y_test, y_pred_binary)
print(f"\nResultats:")
print(f" Precision: {precision:.3f}")
print(f" Recall: {recall:.3f}")
print(f" F1-Score: {f1:.3f}")
Taula comparativa
| Criteri | 60/20/20 Split | 100% Normals |
|---|---|---|
| Dades normals | Milers | Centenars de milers+ |
| Anomalies | Desenes | Centenars (però % baix) |
| Contamination | Ajustat amb validació | Fix (coneixement domini) |
| Cas d’ús | Dataset petit, tuning necessari | Producció fraud/defectes |
| Exemple | 10K motors, 20 defectuosos | 284K transaccions, 492 fraus |
| Validació | Conjunt explícit | Directament al test |
Mètriques d’avaluació
En detecció d’anomalies, la classe positiva (anomalia) és molt minoritària. Per això, la accuracy NO és una bona mètrica. Utilitzem precision, recall i F1-score (veure ml_metrics.md per més detalls).
En funció del context, prioritzarem diferents mètriques:
- Detecció de frau bancari: Preferim alta recall (detectar tots els fraus), acceptant alguns falsos positius que es poden revisar manualment.
- Alertes mèdiques crítiques: Necessitem alta recall (no podem perdre cap cas greu), encara que generi falses alarmes.
- Control de qualitat manufacturera: Equilibri entre precision i recall per no descartar massa productes bons.
Detecció d’anomalies vs aprenentatge supervisat
Quan disposem d’algunes anomalies etiquetades, sorgeix una pregunta natural: per què no utilitzar aprenentatge supervisat en lloc de detecció d’anomalies?
La resposta depèn de diversos factors. Vegem-ne les diferències clau:
Taula comparativa
| Criteri | Detecció d’anomalies | Aprenentatge supervisat |
|---|---|---|
| Exemples positius | Molt pocs (0-50) | Molts (centenars o milers) |
| Tipus d’anomalies | Noves i variades | Conegudes i repetitives |
| Què aprèn | Com és el “normal” | Com són positius i negatius |
| Futurs positius | Poden ser molt diferents | Similars als entrenats |
| Assumpcions | Anomalies futures seran “estranyes” | Patrons coneguts es repetiran |
Quan usar detecció d’anomalies
✅ Usa anomaly detection quan:
- Tens menys de 50 exemples d’anomalies
- Les anomalies futures poden ser de tipus nous (diferents de les observades)
- El cost de no detectar una anomalia nova és molt alt
- Les anomalies són diverses i canvien amb el temps
Exemples:
- Frau financer: Els defraudadors constantment inventen noves tècniques. Un frau detectat avui pot ser molt diferent dels fraus del mes passat.
- Ciberseguretat: Els atacs informàtics evolucionen. Un sistema supervisat només detectaria atacs coneguts, mentre que anomaly detection pot alertar de patrons nous i sospitosos.
- Detecció de defectes nous: En una línia de producció, poden aparèixer tipus de defectes mai vistos abans a causa de nous materials, màquines o processos.
📘 Per a més detalls, consulta l’apèndix: quan usar aprenentatge supervisat i enginyeria de característiques.
Anàlisi de components principals
PCA (Principal Components Analysis) és un algoritme d’aprenentatge no supervisat que s’utilitza sovint per a la visualització i reducció de la dimensionalitat de dades. Si teniu un conjunt de dades amb moltes característiques —per exemple 10, 50 o fins i tot milers—, no és possible representar-lo directament en un gràfic.
El PCA busca noves característiques (anomenades components principals) que capturen la màxima variabilitat de les dades i redueixen el nombre de dimensions a dues o tres, fent possible la seva visualització i una millor comprensió.
Com es construeixen els components? Cada component principal és una combinació lineal de les característiques originals:
z_1 = w_{11} x_1 + w_{12} x_2 + \dots + w_{1n} x_n
Els pesos w es calculen de manera que z_1 capturi la màxima variància possible, z_2 la màxima variància restant (i sigui ortogonal a z_1), i així successivament. A més, es restringeix que la norma dels pesos sigui 1 (w_{11}^2 + w_{12}^2 + \dots = 1): sense aquesta restricció, podríem augmentar la variància indefinidament simplement escalant els pesos. Amb norma 1, PCA busca una direcció en l’espai de característiques, no una magnitud.
Això significa que les noves característiques no són cap de les originals, sinó mescles de totes elles, cosa que pot fer-les difícils d’interpretar.
La maledicció de la dimensionalitat
Una motivació important per usar PCA és evitar la maledicció de la dimensionalitat (curse of dimensionality).
Imagina que vols trobar el veí més proper d’un punt. En 2D, els punts estan relativament a prop i és fàcil identificar quins són similars. Però si afegeixes més dimensions, l’espai creix exponencialment: passa d’una línia a un quadrat, després a un cub, i així successivament. En aquest espai immens, les dades es tornen tan disperses que tots els punts acaben “igual de lluny” entre si. Algoritmes com K-NN o K-means, que depenen de distàncies, deixen de funcionar bé.
A més, per cobrir adequadament un espai amb moltes dimensions necessitem exponencialment més dades. I si no en tenim prou, els models troben patrons espuris que no generalitzen (sobreajust).
PCA ajuda comprimint les dades en un espai més petit i manejable, on les distàncies tornen a ser significatives.
Exemple senzill: cotxes
Suposem que tenim un conjunt de dades de cotxes de passatgers, amb moltes característiques: longitud, amplada, diàmetre de les rodes, alçada, etc.
Si volem reduir el nombre de característiques per a visualització, podem aplicar PCA.
Exemple 1: dues característiques
Suposem que tenim només dues característiques:
- x_1 = longitud del cotxe
- x_2 = amplada del cotxe
A la majoria de països, l’amplada dels cotxes varia poc perquè han de cabre a les carreteres de carril únic. Per exemple, als Estats Units, la majoria de cotxes tenen aproximadament 1,8 m d’amplada.
Si representem gràficament la longitud i l’amplada dels cotxes, trobarem que:
- x_1 varia molt (hi ha cotxes curts i llargs)
- x_2 varia poc
En aquest cas, el PCA automàticament decidiria prioritzar x_1, ja que conté la major part de la informació rellevant.
Exemple 2: longitud i diàmetre de rodes
Ara x_1 és la longitud del cotxe i x_2 és el diàmetre de les rodes.
Encara que x_2 variï una mica, PCA probablement decidirà que x_1 és suficient per representar la variació principal.
Exemple 3: longitud i alçada
Ara x_1 = longitud i x_2 = alçada del cotxe.
Ambdues característiques varien força i contenen informació útil.
En lloc de triar només x_1 o només x_2, PCA crea un nou eix combinat, que anomenarem z.
Aquest eix z és una combinació lineal de x_1 i x_2 que captura la major part de la variació de les dades:
z = w_1 x_1 + w_2 x_2
La coordenada d’un cotxe sobre l’eix z resumeix aproximadament la “grandària total” del cotxe, sense perdre informació rellevant.
Exemple amb més dimensions
Suposem que tenim un conjunt de dades tridimensional amb x_1, x_2, x_3.
Encara que les dades estiguin en 3D, sovint viuen sobre una superfície gairebé bidimensional.
PCA permet reduir de 3 característiques a 2 noves coordenades z_1, z_2 per a visualitzar les dades en un gràfic 2D.
Exemple amb països
Si tenim dades de molts països amb característiques com:
- x_1 = PIB total
- x_2 = PIB per càpita
- x_3 = Índex de Desenvolupament Humà
- …fins a 50 característiques
No podem representar-ho directament en un gràfic.
PCA redueix aquestes 50 característiques a dues o tres:
Z_1, Z_2 = \text{PCA}(X_1, X_2, \dots, X_{50})
Per exemple:
- Z_1 podria correspondre aproximadament a la grandària del país o PIB total
- Z_2 podria correspondre a la riqueza per persona
Això permet visualitzar països grans i petits, amb PIB alt o baix, en un gràfic 2D fàcil d’interpretar.
PCA amb scikit-learn
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
# Normalitzar dades (important per PCA!)
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
# Reduir a 2 dimensions
pca = PCA(n_components=2)
X_reduced = pca.fit_transform(X_scaled)
# Variància explicada per cada component
print(pca.explained_variance_ratio_) # Ex: [0.72, 0.15]
# Això vol dir: PC1 captura 72%, PC2 captura 15% de la variància
Selecció del nombre de components
Un dels aspectes clau de PCA és decidir quants components conservar. Menys components significa un model més simple, però també implica perdre informació. La clau per prendre aquesta decisió és la variància explicada.
Variància explicada (explained variance ratio)
Cada component principal captura una part de la variabilitat total de les dades. L’atribut explained_variance_ratio_ retorna un array on cada valor indica la proporció de variància capturada per aquell component:
- Els valors estan ordenats de major a menor (PC1 sempre captura més que PC2)
- La suma de tots els valors és 1.0 (100% de la variància)
Exemple:
explained_variance_ratio_ = [0.72, 0.15, 0.08, 0.03, 0.02]
→ PC1 sol: 72% de la informació
→ PC1 + PC2: 87% de la informació
→ PC1 + PC2 + PC3: 95% de la informació
Si amb 2 components ja capturem el 87% de la variància, les altres 48 característiques originals eren probablement redundants o molt correlacionades entre si.
Variància retinguda
Quan reduïm de n característiques a k components, “perdem” part de la informació. La variància retinguda és la suma acumulada dels primers k valors de explained_variance_ratio_, i és la mètrica clau per decidir quants components conservar.
Per què descartem les direccions amb poca variància?
La intuïció clau és que variància alta = informació útil. Si una característica (o direcció en l’espai de dades) varia molt poc, aporta poca capacitat per distingir entre observacions:
- Una característica on tots els valors són gairebé iguals no ajuda a diferenciar res
- En canvi, una característica amb molta variació permet separar o agrupar les dades
PCA ordena les direccions de més a menys variància. Quan descartem els últims components (els de baixa variància), estem eliminant:
- Soroll: fluctuacions aleatòries que no aporten informació real
- Redundància: informació ja capturada pels primers components
- Detalls irrellevants: variacions mínimes que no afecten l’anàlisi
Per això, descartar components de baixa variància sovint millora els models: eliminem soroll sense perdre el senyal important.
Hi ha un compromís:
- Més components → més variància retinguda → model més complex
- Menys components → menys variància retinguda → model més simple però pot perdre patrons importants
Els llindars habituals són 90%, 95% o 99%, depenent de l’aplicació.
Criteris segons el propòsit
| Propòsit | Components recomanats | Raonament |
|---|---|---|
| Visualització | 2-3 | Permet representar les dades en un gràfic 2D o 3D |
| Compressió | 90-99% variància | Reté la informació essencial amb menys espai |
| Preprocessat per ML | Validació creuada | Trobar el punt òptim entre retenir senyal i descartar soroll |
PCA com a tècnica de regularització:
PCA no només serveix per reduir dimensions o visualitzar. També pot millorar el rendiment d’un model predictiu. Com? Els components de baixa variància sovint contenen més soroll que senyal. En descartar-los:
- Reduïm el sobreajust (overfitting): el model no aprèn patrons espuris del soroll
- Millorem la generalització: el model es concentra en les característiques realment informatives
- Accelerem l’entrenament: menys dimensions significa menys càlcul
En alguns casos, retenir menys del 90% de la variància pot donar millors resultats que usar totes les característiques originals. L’única manera de saber-ho és validar experimentalment amb cross-validation.
Selecció automàtica amb scikit-learn
import numpy as np
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
# Normalitzar dades
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
# Opció 1: Selecció automàtica per llindar de variància
pca = PCA(n_components=0.95) # Retenir 95% de la variància
X_reduced = pca.fit_transform(X_scaled)
print(f"Components seleccionats: {pca.n_components_}")
# Ex: "Components seleccionats: 10" (de 50 originals)
# Opció 2: Explorar variància acumulada manualment
pca_full = PCA()
pca_full.fit(X_scaled)
variancia_acumulada = np.cumsum(pca_full.explained_variance_ratio_)
print(variancia_acumulada)
# Ex: [0.72, 0.87, 0.95, 0.98, 1.0]
# → Amb 3 components ja tenim el 95%
Consells pràctics
- Per defecte, comença amb un llindar del 95% de variància retinguda
- Per visualització, utilitza sempre 2 o 3 components
- Per models predictius, valida amb cross-validation sobre la tasca final: a vegades menys components (que eliminen soroll) milloren el rendiment
Resum
PCA és un algoritme potent per reduir la dimensionalitat:
- Pren dades amb moltes característiques (alta dimensionalitat)
- Les redueix a 2 o 3 característiques principals
- Permet visualitzar i entendre millor les dades
És especialment útil per explorar conjunts de dades complexos, detectar patrons i anomalies, i preparar les dades per altres algorismes d’aprenentatge automàtic.
Errors comuns a evitar
Aquests són alguns errors freqüents quan s’apliquen tècniques d’aprenentatge no supervisat:
K-means:
- Escollir K arbitràriament sense usar el mètode del colze o considerar el context
- No normalitzar les característiques (les variables amb escales grans dominaran)
- Executar l’algoritme només una vegada (pot quedar atrapat en mínims locals)
Detecció d’anomalies:
- Configurar malament el paràmetre de contaminació (massa falsos positius o falsos negatius)
- No escalar o preparar adequadament les característiques quan tenen escales molt diferents
- Interpretar l’anomaly score com una probabilitat
PCA:
- Oblidar normalitzar les dades abans d’aplicar PCA (imprescindible!)
- Reduir a massa poques dimensions i perdre informació important
- Interpretar els components principals com si fossin les variables originals
Apèndix: Detecció d’anomalies (ampliació)
Aquesta secció conté material addicional sobre detecció d’anomalies per a qui vulgui aprofundir en el tema.
Quan usar aprenentatge supervisat
✅ Usa supervised learning quan:
- Tens centenars o milers d’exemples positius
- Les anomalies futures seran similars a les passades
- Els patrons són repetitius i estables
- Necessites alta precisió en casos coneguts
Exemples:
- Classificació de correu brossa: Els correus spam tendeixen a repetir patrons similars (ofertes, enllaços sospitosos, majúscules excessives). Els futurs spams seran similars als passats.
- Diagnòstic mèdic de malalties conegudes: Els símptomes d’una pneumònia o diabetis són estables i ben documentats.
- Detecció de defectes coneguts: Si una fàbrica de smartphones sap que el 2% de pantalles tenen un ratllat específic per una màquina defectuosa, pot entrenar un classificador per detectar aquest defecte concret.
Taula d’exemples aplicats
| Aplicació | Recomanació | Raó |
|---|---|---|
| Frau amb targeta de crèdit | Anomaly detection | Noves tècniques de frau emergeixen constantment |
| Classificació de correu brossa | Supervised learning | Patrons de spam són repetitius i coneguts |
| Defectes nous en fabricació | Anomaly detection | Tipus de defecte imprevisibles i canviants |
| Diagnòstic de grip vs pneumònia | Supervised learning | Símptomes ben establerts i documentats |
| Detecció d’intrusions a xarxes | Anomaly detection | Atacs nous i sofisticats apareixen regularment |
| Predicció meteorològica | Supervised learning | Només hi ha uns quants tipus de temps coneguts |
Enfocament híbrid
En alguns casos, es poden combinar ambdues tècniques:
- Fase 1: Usar anomaly detection per identificar casos sospitosos
- Fase 2: Revisió manual i etiquetatge d’aquests casos
- Fase 3: Quan s’acumulin prou exemples (>100), entrenar un model supervisat per als patrons coneguts
- Fase 4: Mantenir anomaly detection per detectar nous tipus no coberts pel supervisat
Aquest enfocament aprofita el millor de cada món: eficiència per patrons coneguts i capacitat de detectar anomalies noves.
Enginyeria de característiques
En detecció d’anomalies, l’elecció de bones característiques és crítica. A diferència de l’aprenentatge supervisat, on el model pot aprendre quines característiques ignorar, en anomaly detection no tenim senyal supervisada per guiar aquest procés.
Per què és tan important?
Isolation Forest (i altres mètodes no supervisats) no saben què fa que una observació sigui anòmala sense que nosaltres els hi indiquem a través de les característiques.
Exemple: En detecció de frau, tenir només “import de transacció” podria no ser suficient, perquè una transacció de 1.000€ pot ser normal per un client i anòmala per un altre. Però la característica import / mitjana_habitual_usuari és molt més informativa.
Creació de característiques rellevants
Principi general: Crear característiques que capturin desviacions del comportament normal.
A continuació es mostren alguns exemples per dominis.
Targetes de crèdit:
# Característiques bàsiques
df['hour_of_day'] = df['timestamp'].dt.hour
df['day_of_week'] = df['timestamp'].dt.dayofweek
# Característiques relatives a l'usuari
df['amount_vs_avg'] = df['amount'] / df.groupby('user_id')['amount'].transform('mean')
df['time_since_last'] = df.groupby('user_id')['timestamp'].diff().dt.total_seconds()
# Característiques de freqüència
df['transactions_last_hour'] = df.groupby('user_id').rolling('1H', on='timestamp').count()
Monitoratge de servidors:
# Ràtios que capten comportaments anòmals
df['cpu_per_network'] = df['cpu_load'] / (df['network_traffic'] + 1) # +1 evita divisió per 0
df['memory_growth_rate'] = df.groupby('server_id')['memory_used'].diff()
df['disk_io_vs_cpu'] = df['disk_io'] / (df['cpu_load'] + 0.01)
Fabricació:
# Relacions entre variables físiques
df['temp_vibration_ratio'] = df['temperature'] / (df['vibration'] + 0.001)
df['pressure_deviation'] = np.abs(df['pressure'] - df['pressure'].rolling(10).mean())
Transformacions de distribucions
Moltes vegades, les característiques originals tenen distribucions molt esbiaixades (skewed). Aplicar transformacions pot ajudar els algoritmes a funcionar millor.
A continuació es mostren transformacions habituals:
| Transformació | Quan usar-la | Exemple |
|---|---|---|
log(X + c) | Distribució molt esbiaixada a la dreta | Imports monetaris, freqüències |
sqrt(X) | Esbiaixament moderat | Comptadors, taxes |
X^(1/3) o altres potències | Valors extrems molt grans | Volums, poblacions |
1/X | Relació inversa | Temps entre esdeveniments |
⚠️ Important: Qualsevol transformació aplicada al conjunt d’entrenament s’ha d’aplicar també als conjunts de validació i test.
Anàlisi d’errors iteratiu
El procés de millorar un sistema de detecció d’anomalies és iteratiu:
Workflow recomanat:
- Entrenar model amb característiques actuals
- Avaluar sobre validation set (amb anomalies etiquetades)
- Identificar FALSOS NEGATIUS (anomalies no detectades)
- Analitzar què diferencia aquests casos dels normals
- Crear noves característiques que captin aquestes diferències
- Tornar a 1
Exemple pràctic:
Suposem que el nostre model no detecta un servidor compromès que està enviant dades a l’exterior.
Anàlisi:
- CPU: Normal (3.2 GHz) ✓
- Memòria: Normal (40% usat) ✓
- Tràfic de xarxa: Alt però no extremadament (podria ser legítim) ⚠️
Solució: Crear una nova característica:
# Ràtio de tràfic de xarxa vs càrrega de CPU
df['network_per_cpu'] = df['network_traffic'] / (df['cpu_load'] + 0.01)
Aquesta característica serà anormalment alta per un servidor que envia dades sense fer càlculs intensius, permetent detectar l’anomalia.
Normalització i escalat
Isolation Forest és relativament robust a escales diferents, però sovint és recomanable normalitzar les característiques:
from sklearn.preprocessing import StandardScaler
# Normalitzar (mitjana=0, desviació estàndard=1)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_val_scaled = scaler.transform(X_val) # Usar mateixos paràmetres!
# Entrenar amb dades normalitzades
clf = IsolationForest(contamination=0.05, random_state=42)
clf.fit(X_train_scaled)
Això és especialment important si les característiques tenen unitats molt diferents (ex: temperatura en ºC vs pressió en Pa).
Consells pràctics i workflow complet
1. Context de negoci primer
L’algorisme més sofisticat no serveix de res sense coneixement del domini:
- Parla amb experts del sector (enginyers, auditors, metges…)
- Entén què fa que una observació sigui realment problemàtica
- Defineix el cost dels falsos positius vs falsos negatius
Pregunta clau: És millor revisar 100 casos normals per no perdre 1 anomalia? O preferim perdre algunes anomalies per evitar sobrecàrrega de revisions?
2. Comença simple, itera ràpid
No intentis crear el model perfecte des del principi:
Iteració 1: 2-3 característiques bàsiques
→ Avalua → Identifica errors
Iteració 2: Afegeix 1-2 característiques noves
→ Avalua → Millora?
Iteració 3: Prova transformacions
→ Avalua → Ajusta contamination
Cada iteració hauria de durar minuts o hores, no dies.
3. Quan considerar aprenentatge supervisat
Si després d’uns mesos d’ús del sistema tens:
- Més de 100 anomalies etiquetades → Planteja’t un model supervisat complementari
- Patrons repetitius clars → Random Forest o XGBoost poden funcionar millor
- Nous tipus d’anomalies emergeixen → Manté l’anomaly detection com a primera línia
Enfocament híbrid recomanat:
- Isolation Forest detecta casos sospitosos
- Revisió humana etiqueta aquests casos
- Model supervisat per patrons coneguts
- Isolation Forest per detectar patrons nous
Sistemes de recomanació
- Introducció
- Mètriques d’avaluació
- Predictors Baseline
- Filtrat basat en contingut
- Filtrat col·laboratiu
- Tècniques avançades
- Com evolucionar el recomanador
- Oportunitats i riscos
Introducció
Què és un sistema de recomanació?
Farem servir com a exemple conductor el problema de predir les valoracions de pel·lícules.
Imaginem que tens una plataforma de streaming i que els teus usuaris puntuen les pel·lícules d’1 a 5 estrelles. En un sistema típic de recomanació tenim:
- Un conjunt d’usuaris (per exemple: Alice, Bob, Carol i Dave, numerats de 1 a 4).
- Un conjunt d’ítems (en aquest cas, pel·lícules: Love at Last, Romance Forever, Cute Puppies of Love, Nonstop Car Chases, Sword vs. Karate).
- Les puntuacions que cada usuari dona (o no dona) a cadascuna de les pel·lícules.
Per simplicitat, suposem que les valoracions poden anar de 0 a 5 estrelles (0 = “gens interessant”). Quan un usuari encara no ha puntuat una pel·lícula, ho representarem amb un “?”.
Exemple:
| Movie | Alice (1) | Bob (2) | Carol (3) | Dave (4) |
|---|---|---|---|---|
| Love at last | 5 | 5 | 0 | 0 |
| Romance forever | 5 | ? | ? | 0 |
| Cute puppies of love | ? | 4 | 0 | ? |
| Nonstop car chases | 0 | 0 | 5 | 4 |
| Swords vs. karate | 0 | 0 | 5 | ? |
- Alice puntua Love at Last amb 5 estrelles i Romance Forever amb 5, però no ha vist Cute Puppies of Love (per tant, “?”).
- Bob dona 5 estrelles a Love at Last, 4 a Cute Puppies of Love, i 0 a dues altres pel·lícules.
- Carol posa 0 estrelles a dues, però li encanten Nonstop Car Chases i Sword vs. Karate.
- Dave té un altre patró de puntuacions.
Notació
Per generalitzar:
-
Direm que hi ha n_u usuaris. En el nostre exemple, n_u = 4 .
-
Direm que hi ha n_m pel·lícules (o ítems en general). En el nostre exemple, n_m = 5 .
-
Definim una variable r(i,j) :
r(i,j) = \begin{cases} 1 & \text{si l’usuari } j \text{ ha puntuat la pel·lícula } i \\ 0 & \text{si l’usuari } j \text{ no l’ha puntuada} \end{cases}
Exemple:
- Alice (usuari 1) ha puntuat la pel·lícula 1 → r(1,1) = 1 .
- Però no ha puntuat la pel·lícula 3 → r(3,1) = 0 .
-
Definim y(i,j) com la puntuació donada per l’usuari j a la pel·lícula i . Exemple: si l’usuari 2 dona un 4 a la pel·lícula 3, llavors y(3,2) = 4 .
Aquesta distinció és important: no tots els usuaris puntuen totes les pel·lícules. Saber quines puntuacions existeixen i quines no, és clau pel sistema.
Sparsity: En sistemes reals, la immensa majoria de caselles són “?” (sense valorar). Per exemple, a Netflix cada usuari veu ~100 pel·lícules d’un catàleg de desenes de milers, deixant >99% de la matriu buida. Això té implicacions importants tant per l’algorisme (com aprendre amb tan poques dades?) com per la implementació (emmagatzematge eficient amb sparse matrices).
Quin és l’objectiu?
Donat que hi ha moltes caselles buides (pel·lícules no puntuades), un sistema de recomanació ha d’intentar predir quina puntuació posaria un usuari a una pel·lícula que encara no ha vist.
A partir d’aquestes prediccions podem suggerir als usuaris les pel·lícules (o productes, restaurants, articles…) que és més probable que valorin amb 5 estrelles.
Assumpció inicial
Per començar a desenvolupar un algoritme, farem una suposició especial: suposarem que tenim característiques addicionals de les pel·lícules, com ara si són de gènere romàntic o d’acció.
Amb aquesta informació extra podrem començar a construir un primer algoritme. Més endavant, veurem què passa si no disposem d’aquestes característiques i haurem de fer que el sistema aprengui igualment.
Mètriques d’avaluació
Necessitem mètriques diferents segons la natura de la predicció: ratings numèrics vs llistes ordenades d’ítems.
Mètriques per a prediccions de ratings
Per sistemes que prediuen ratings numèrics (1-5 estrelles), fem servir:
- RMSE (Root Mean Squared Error): Penalitza errors grans. Netflix Prize va usar RMSE com a mètrica oficial.
- MAE (Mean Absolute Error): Menys sensible a valors atípics que RMSE.
Vegeu ml_metrics.md per a les fórmules i explicacions detallades.
Exemple: Si predim 4.2 estrelles per un usuari que dona 5, l’error és 0.8. RMSE eleva aquest error al quadrat (0.64), penalitzant-lo més que MAE (0.8).
Mètriques per a recomanació de llistes (ranking)
Quan el sistema recomana una llista ordenada d’ítems (top-10 pel·lícules recomanades), necessitem mètriques específiques que avaluïn la qualitat del ranking.
Precision@K i Recall@K
Precision@K respon: “dels K ítems que he recomanat, quants eren bons?”
Recall@K respon: “dels ítems bons que existeixen, quants he recomanat?”
Direm que un ítem és rellevant quan rating ≥ 4 estrelles (explícit) o interacció com clic/compra (implícit).
\text{Precision@K} = \frac{\text{Ítems rellevants en top-K}}{K}
Intuïció de la fórmula: El denominador és K (el nombre total de recomanacions que hem fet). El numerador compta quantes d’aquestes recomanacions eren bones. Per tant, Precision@K mesura la qualitat de les recomanacions: quin percentatge del que hem recomanat és realment útil.
\text{Recall@K} = \frac{\text{Ítems rellevants en top-K}}{\text{Total ítems rellevants}}
Intuïció de la fórmula: El denominador és el total d’ítems que li agraden a l’usuari (tots els que tenen ≥4 estrelles, per exemple). El numerador compta quants d’aquests hem aconseguit incloure a les nostres K recomanacions. Per tant, Recall@K mesura la cobertura: quin percentatge dels ítems bons hem “rescatat” o trobat.
Nota sobre Recall@K: Recall@K té un límit superior natural: si l’usuari té 100 pel·lícules que li agraden i només recomanem K=10, el màxim Recall@10 possible és 10/100 = 0.10 (10%). Per això, valors “baixos” de Recall@K no són necessàriament dolents — cal interpretar-los en funció de K i del total d’ítems rellevants.
Exemple concret:
Imaginem que en Eva té 15 pel·lícules que li agraden (va donar ≥ 4 estrelles) d’un catàleg de 100 pel·lícules. El nostre sistema li recomana una llista de 10 pel·lícules (K=10), de les quals 6 són pel·lícules que li agraden.
- Precision@10 = 6/10 = 0.60 → El 60% de les recomanacions són bones
- Recall@10 = 6/15 = 0.40 → Hem cobert el 40% de totes les pel·lícules que li agraden
Si canviem a K=20 i ara cobrim 9 pel·lícules bones:
- Precision@20 = 9/20 = 0.45 → Baixa (diluïm amb més recomanacions)
- Recall@20 = 9/15 = 0.60 → Puja (cobrim més pel·lícules bones)
Comparació amb random: Si recomanéssim 10 pel·lícules a l’atzar d’un catàleg de 100, donat que la probabilitat d’encertar-hi una és 15/100, esperaríem encertar 10 × (15/100) = 1.5 pel·lícules bones de mitjana:
- Precision@10 random = 1.5/10 = 0.15 (15%)
- Recall@10 random = 1.5/15 = 0.10 (10%)
El nostre model (Precision 0.60, Recall 0.40) és 4× millor que random — senyal que el model aporta valor real.
NDCG@K (Normalized Discounted Cumulative Gain)
Mètrica que penalitza quan ítems rellevants apareixen més avall a la llista. A diferència de Precision@K, NDCG considera que la posició importa: recomanar un bon ítem al lloc 1 és millor que al lloc 10. NDCG ∈ [0, 1], on 1 = ordre perfecte.
Intuïció: Si recomano [pel·lícula excel·lent, excel·lent, dolenta, dolenta] és millor que [dolenta, dolenta, excel·lent, excel·lent], encara que Precision@4 sigui igual.
Taula resum: Què mesurar segons la tasca
| Tasca | Mètrica Principal | Mètriques Secundàries |
|---|---|---|
| Predir ratings explícits | RMSE | MAE, R² |
| Recomanar top-N ítems | Precision@K, Recall@K | NDCG@K, F1@K |
Mètriques offline vs online
Les mètriques offline (RMSE, Precision@K) es calculen sobre dades històriques. Les mètriques online (CTR, conversions) mesuren comportament real en producció. Important: Un model amb millor RMSE no sempre funciona millor amb usuaris reals.
Predictors Baseline
Abans de construir models sofisticats, és fonamental establir predictors simples que serveixin com a punt de referència.
Per què són importants els baselines?
Si un model complex no millora un baseline simple, probablement:
- Té errors d’implementació
- Està sobreajustat
- No està ben configurat
Els baselines també són útils com a fallback: quan tenim un usuari nou sense gairebé dades, podem usar el baseline mentre recopilam més informació.
Tipus de baselines
1. Predicció per mitjana global
El predictor més simple possible:
\hat{y}(i,j) = \mu
on \mu és la mitjana de totes les valoracions del dataset.
Exemple: Si la mitjana de totes les valoracions és 3.5, predeiem 3.5 estrelles per a qualsevol parella usuari-ítem. Simple però ignora preferències individuals i qualitat dels ítems.
2. Predicció per mitjana d’ítem
Millora el baseline anterior tenint en compte que alguns ítems són objectivament millors que altres:
\hat{y}(i,j) = \mu_i
on \mu_i és la mitjana de les valoracions de l’ítem i.
Exemple: Si The Shawshank Redemption té una mitjana de 4.8 estrelles, predeiem 4.8 per a qualsevol usuari. Captura la qualitat dels ítems però ignora preferències individuals.
3. Predicció per mitjana d’usuari
Considera que alguns usuaris són més generosos o més crítics en les seves valoracions:
\hat{y}(i,j) = \mu_j
on \mu_j és la mitjana de les valoracions de l’usuari j.
Exemple: Si l’Alice normalment valora amb 4-5 estrelles (generosa) i en Bob amb 1-2 (crític), això es reflecteix en les prediccions. Captura comportament individual però ignora qualitat dels ítems.
4. Model de bias (usuari + ítem)
El millor baseline combina els dos efectes anteriors:
\hat{y}(i,j) = \mu + b_i + b_j
on:
- \mu és la mitjana global de totes les valoracions
- b_i = \mu_i - \mu és el bias de l’ítem (desviació de l’ítem respecte la mitjana global)
- b_j = \mu_j - \mu és el bias de l’usuari (desviació de l’usuari respecte la mitjana global)
Exemple detallat:
- Mitjana global: \mu = 3.5
- The Shawshank Redemption: \mu_i = 4.8 → b_i = +1.3 (pel·lícula excel·lent)
- Alice (generosa): \mu_j = 4.2 → b_j = +0.7 (usuària que valora alt)
- Predicció: \hat{y} = 3.5 + 1.3 + 0.7 = 5.5
- Com el màxim és 5, la predicció final és 5 estrelles
Altre exemple:
- Mitjana global: \mu = 3.5
- Pel·lícula mediocre: \mu_i = 2.8 → b_i = -0.7
- Bob (crític): \mu_j = 2.5 → b_j = -1.0
- Predicció: \hat{y} = 3.5 - 0.7 - 1.0 = 1.8 → ~2 estrelles
Càlcul dels bias: El bias de l’ítem és simplement la diferència entre la seva mitjana i la mitjana global. El bias de l’usuari es calcula de manera anàloga.
Quan usar cada baseline?
| Situació | Baseline recomanat |
|---|---|
| Usuari nou, ítem conegut | Mitjana d’ítem (\mu_i) |
| Usuari conegut, ítem nou | Mitjana d’usuari (\mu_j) |
| Usuari i ítem nous | Mitjana global (\mu) |
| Usuari i ítem coneguts | Model de bias (\mu + b_i + b_j) |
Filtrat basat en contingut
El filtrat basat en contingut utilitza característiques dels ítems (i opcionalment dels usuaris) per fer recomanacions, en lloc de dependre exclusivament de les valoracions d’altres usuaris. Si disposem de característiques dels ítems (per exemple, gènere de pel·lícules), podem construir un model personalitzat per a cada usuari.
Exemple amb característiques
Partim del mateix conjunt de dades amb 4 usuaris que han valorat algunes de les 5 pel·lícules. Ara, a més, suposem que disposem de característiques de cada pel·lícula. Per exemple, definim:
- X₁: grau en què la pel·lícula és romàntica.
- X₂: grau en què la pel·lícula és d’acció.
| Movie | Alice (1) | Bob (2) | Carol (3) | Dave (4) | X₁ (romance) | X₂ (action) |
|---|---|---|---|---|---|---|
| Love at last | 5 | 5 | 0 | 0 | 0.9 | 0 |
| Romance forever | 5 | ? | ? | 0 | 1.0 | 0.01 |
| Cute puppies of love | ? | 4 | 0 | ? | 0.99 | 0 |
| Nonstop car chases | 0 | 0 | 5 | 4 | 0.1 | 1.0 |
| Swords vs. karate | 0 | 0 | 5 | ? | 0 | 0.9 |
Exemples:
- Love at Last: molt romàntica (X₁=0.9), gens d’acció (X₂=0).
- Nonstop Car Chases: una mica romàntica (X₁=0.1), molt d’acció (X₂=1.0).
- Cute Puppies of Love: gairebé totalment romàntica (X₁=0.99), gens d’acció (X₂=0).
Ja havíem definit:
- n_u = 4: nombre d’usuaris.
- n_m = 5: nombre de pel·lícules.
Ara afegim:
- n = 2: nombre de característiques de cada pel·lícula (X₁ i X₂).
Predicció per a un usuari
Per l’usuari 1 (Alice), volem predir la seva valoració d’una pel·lícula i. Definim:
\hat{y}^{(1)}(i) = w^{(1)} \cdot X(i) + b^{(1)}
on w^{(1)} \cdot X(i) és el producte escalar (dot product) entre el vector de preferències de l’usuari i el vector de característiques de la pel·lícula.
Això és molt semblant a una regressió lineal.
Exemple: si prenem
- w^{(1)} = [5, 0]
- b^{(1)} = 0,
llavors per a la pel·lícula 3 (Cute Puppies of Love) amb X(3) = [0.99, 0]:
\hat{y}^{(1)}(3) = 5 \cdot 0.99 + 0 \cdot 0 = 4.95
Aquest resultat és raonable: Alice valora molt positivament les pel·lícules romàntiques i poc les d’acció.
Per això, per a cada usuari j, tindrem un conjunt propi de paràmetres w^{(j)} i b^{(j)}.
Model general
Per tant, la predicció de la valoració de l’usuari j sobre la pel·lícula i és:
\hat{y}^{(j)}(i) = w^{(j)} \cdot X(i) + b^{(j)}
on:
- X(i) és el vector de característiques de la pel·lícula i.
- w^{(j)}, b^{(j)} són els paràmetres específics de l’usuari j.
Així, és com si entrenéssim una regressió lineal diferent per a cada usuari.
Entrenament
Volem trobar w i b que minimitzin l’error entre prediccions i valoracions reals. Això és exactament una regressió lineal per a cada usuari, amb regularització per evitar sobreajustament.
En essència: minimitzem (predicció - real)² + penalització per pesos grans.
Per entrenar tots els usuaris alhora, simplement sumem el cost de cada usuari i optimitzem amb descens de gradient.
Però… d’on surten aquestes característiques? Sovint no disposem de característiques prèvies tan clares. En aquest cas, necessitarem aprenentatge automàtic per descobrir característiques latents (filtrat col·laboratiu).
Filtrat col·laboratiu
La idea clau: si molts usuaris que t’assemblen van valorar bé una pel·lícula, probablement a tu també t’agradarà. Això és “col·laboratiu” perquè els usuaris col·laboren indirectament a través de les seves valoracions.
Però per fer-ho, necessitem representar cada usuari i cada pel·lícula amb vectors numèrics. Si no tenim característiques prèvies x_1, x_2 per a les pel·lícules, les podem aprendre automàticament a partir de les dades.
Diferència clau amb l’aprenentatge supervisat: En un problema de classificació o regressió típic, les característiques x vénen donades i el model aprèn els pesos w. En el filtrat col·laboratiu, no tenim característiques prèvies — el model ha d’aprendre tant els vectors dels usuaris com els dels ítems. Aquestes representacions apreses s’anomenen factors latents o embeddings.
Per entendre com funciona, ho construirem en tres passos:
Pas 1: Suposem que coneixem els vectors
Per entendre com funciona la predicció, primer suposarem que ja tenim els vectors dels usuaris w i els vectors dels ítems x. Més endavant veurem d’on surten.
Imaginem que disposem de vectors w per a cada usuari i vectors x per a cada pel·lícula (més un biaix per usuari i un per ítem). Les dues dimensions corresponen als factors latents que capturen preferències: en aquest exemple simplificat, podem interpretar-los com “romanticisme” i “acció”.
- Usuari 1 (Alice): w^{(1)} = (5, 0) → li encanta el romanticisme, no li agrada l’acció
- Usuari 2 (Bob): w^{(2)} = (5, 0) → mateix perfil que Alice
- Usuari 3 (Carol): w^{(3)} = (0, 5) → no li agrada el romanticisme, li encanta l’acció
- Usuari 4 (Dave): w^{(4)} = (0, 5) → mateix perfil que Carol
Per simplicitat, suposem que tots els biaixos són zero: b_u^{(j)} = 0, b_i^{(i)} = 0.
La predicció de la valoració de l’usuari j sobre la pel·lícula i és:
\hat{y}(i,j) = w^{(j)} \cdot x^{(i)} + b_u^{(j)} + b_i^{(i)} \approx w^{(j)} \cdot x^{(i)}
on \hat{y}(i,j) és la predicció de la valoració que l’usuari j donaria a la pel·lícula i.
Intuïció del producte escalar: El producte escalar és alt quan els dos vectors “apunten en la mateixa direcció”. Si l’usuari té w = (5, 0) (li encanta el romanticisme) i la pel·lícula té x = (1, 0) (molt romàntica), el producte escalar serà alt → predicció alta → recomanem!
Exemple: Predim la valoració d’Alice per Love at Last (x = (0.9, 0), molt romàntica):
\hat{y} = w^{(1)} \cdot x = (5, 0) \cdot (0.9, 0) = 5 \times 0.9 + 0 \times 0 = 4.5 \text{ estrelles}
I per Nonstop Car Chases (x = (0.1, 1.0), molt d’acció):
\hat{y} = (5, 0) \cdot (0.1, 1.0) = 5 \times 0.1 + 0 \times 1.0 = 0.5 \text{ estrelles}
Les prediccions tenen sentit: Alice valorarà alt les pel·lícules romàntiques i baix les d’acció.
Pas 2: Deduir els vectors dels ítems
Ara invertim el problema: si coneixem els vectors dels usuaris w, podem deduir els vectors dels ítems x?
Suposem que Love at Last (pel·lícula 1) va rebre aquestes valoracions:
- Alice i Bob (amants del romanticisme): 5 estrelles
- Carol i Dave (amants de l’acció): 0 estrelles
Quin vector x^{(1)} fa que les prediccions s’ajustin a aquestes valoracions?
- w^{(1)} \cdot x^{(1)} = (5, 0) \cdot x^{(1)} \approx 5 → Alice
- w^{(2)} \cdot x^{(1)} = (5, 0) \cdot x^{(1)} \approx 5 → Bob
- w^{(3)} \cdot x^{(1)} = (0, 5) \cdot x^{(1)} \approx 0 → Carol
- w^{(4)} \cdot x^{(1)} = (0, 5) \cdot x^{(1)} \approx 0 → Dave
La solució és x^{(1)} = (1, 0): plenament romàntica i gens d’acció. Comprovem:
- Alice: (5, 0) \cdot (1, 0) = 5 ✓
- Carol: (0, 5) \cdot (1, 0) = 0 ✓
Intuïció clau: Els usuaris que estimen el romanticisme li donen 5 estrelles, i els que estimen l’acció li donen 0. Això ens diu que la pel·lícula és romàntica! El patró de valoracions revela les característiques de l’ítem.
De la mateixa manera, per a Nonstop Car Chases (on Carol i Dave donen 5 i Alice i Bob donen 0), deduiríem x = (0, 1): gens romàntica, plenament d’acció.
Al filtrat col·laboratiu, podem esbrinar els vectors dels ítems a partir dels vectors dels usuaris. Això és possible gràcies a tenir múltiples usuaris amb preferències diferents. Amb només un usuari, no hi hauria prou informació per deduir res.
Pas 3: Aprendre tots els vectors alhora
Recapitulem: al Pas 1 hem suposat que coneixíem w i hem vist com predir. Al Pas 2 hem vist que, si coneixem w, podem deduir x. Tenim un problema circular: per deduir x necessitem w, i per deduir w necessitaríem x. En realitat, no coneixem ni w (usuaris) ni x (ítems). La solució: aprendre’ls tots alhora!
Funció de cost: Minimitzem l’error quadràtic entre les prediccions i les valoracions reals, només per a les parelles usuari-ítem que tenen un rating associat:
J = \sum_{(i,j) \in \text{observats}} \left( y_{i,j} - \hat{y}(i,j) \right)^2 + \lambda \left( \|W\|^2 + \|X\|^2 \right)
El terme de regularització \lambda evita que els vectors creixin massa (sobreajustament). Optimitzem amb descens de gradient — exactament com faríem amb una xarxa neuronal.
Intuïció geomètrica: com es posicionen els vectors?
Recordem que la predicció (ignorant biaixos) és \hat{y}(i,j) \approx w^{(j)} \cdot x^{(i)}. El producte escalar és alt (predicció alta) quan els dos vectors apunten en la mateixa direcció, i baix (predicció baixa) quan són ortogonals o oposats. El descens de gradient aprofita això:
- Valoracions altes → el gradient ajusta els vectors perquè s’alineïn més.
- Valoracions baixes → el gradient ajusta els vectors perquè siguin més ortogonals.
Com a resultat, usuaris amb gustos similars acaben amb vectors w semblants, i ítems valorats de manera similar acaben amb vectors x semblants — emergeix un clustering natural a l’espai d’embeddings.
Dimensió dels embeddings: El nombre de factors (la longitud dels vectors) és un hiperparàmetre. Valors típics són 32-128. Més dimensions permeten capturar patrons més complexos però augmenten el risc de sobreajustament i el cost computacional.
Factorització de matrius: Aquesta tècnica s’anomena factorització de matrius (matrix factorization) perquè estem descomponent la matriu de valoracions Y en el producte de dues matrius més petites:
Y \approx W \cdot X^T
on W conté els vectors de tots els usuaris (una fila per usuari) i X conté els vectors de tots els ítems (una fila per ítem). A la pràctica, optimitzem aquests vectors amb descens de gradient — exactament el que fan les xarxes neuronals amb nn.Embedding a PyTorch.
Biaixos d’usuari i d’ítem
A més dels embeddings, molts models aprenen un biaix per a cada usuari i cada ítem. Això captura tendències globals: usuaris que puntuen alt o baix de mitjana, i ítems que reben puntuacions sistemàticament altes o baixes.
Per què és útil? Sense biaixos, l’embedding hauria de codificar què t’agrada (romanticisme vs acció) i com de generós ets puntuant. Amb biaixos, aquestes dues coses se separen: l’embedding captura el gust, el biaix captura la tendència. Això manté l’espai latent “net” — usuaris amb gustos similars queden propers, independentment de si puntuen alt o baix — i millora la precisió de les prediccions.
Nota: Un biaix és un escalar, no un vector multidimensional com els embeddings que capturen factors latents. Tot i això, a PyTorch sovint s’implementen amb nn.Embedding(dim=1) per comoditat.
Intuïció del Filtrat col·laboratiu
Aquest mètode rep el nom de filtrat col·laboratiu perquè:
- Diversos usuaris han valorat les mateixes pel·lícules.
- Això permet deduir característiques implícites de les pel·lícules.
- Amb aquestes característiques, podem predir com valorarà un nou usuari una pel·lícula que encara no ha vist.
En resum:
- En molts algoritmes de ML les característiques han de ser donades externament.
- Aquí, les característiques s’aprenen automàticament a partir de les dades dels usuaris.
Resum de conceptes nous:
- Factors latents: Dimensions ocultes que capturen patrons (romanticisme, acció…). No els definim nosaltres — els descobreix el model.
- Factorització de matrius: Descompondre Y \approx W \cdot X^T per aprendre els vectors d’usuaris i ítems simultàniament.
- Embedding: Vector numèric dens que representa un usuari o ítem. És el terme que fan servir les xarxes neuronals per referir-se als factors latents.
El terme “embedding”
Hem vist que el filtrat col·laboratiu aprèn:
- Un vector x^{(i)} per a cada ítem (les seves característiques latents).
- Un vector w^{(j)} per a cada usuari (els seus “pesos” o preferències).
En el món de les xarxes neuronals, aquests vectors apresos s’anomenen embeddings:
- x^{(i)} és l’embedding de l’ítem.
- w^{(j)} és l’embedding de l’usuari.
Un embedding és simplement una representació numèrica densa d’un objecte (usuari, ítem, paraula…) que el model aprèn durant l’entrenament. A PyTorch, es defineixen amb nn.Embedding(num_objects, embedding_dim), que crea una taula de vectors on cada fila correspon a un objecte. Quan passem l’ID d’un usuari o ítem, obtenim el seu vector embedding.
Nota terminològica: Aquests vectors apresos també s’anomenen “factors latents” (latent factors) en alguna bibliografia. En aquest curs usarem “embedding” de manera consistent.
Comparant vectors: predicció vs similitud
Amb els embeddings apresos podem fer dues operacions diferents:
| Operació | Què compara | Propòsit | Mètode |
|---|---|---|---|
| Predicció | Usuari ↔ Ítem | Predir valoració | Producte escalar w^{(j)} \cdot x^{(i)} |
| Similitud d’ítems | Ítem ↔ Ítem | Trobar ítems similars | Distància euclidiana |
| Similitud d’usuaris | Usuari ↔ Usuari | Trobar usuaris similars | Distància euclidiana |
La predicció (producte escalar entre usuari i ítem) ja l’hem vist: ens diu quant agradarà un ítem a un usuari. Ara veurem com usar la similitud entre ítems per recomanar productes relacionats.
Trobant ítems similars
Un cop tenim els embeddings dels ítems, podem aprofitar-los per trobar ítems similars.
Imagina que entres en una botiga en línia i mires un producte concret, per exemple, un llibre. Sovint, la web et mostra missatges com: “Aquí tens altres llibres semblants a aquest” o bé, si estàs mirant una pel·lícula: “Aquí tens altres pel·lícules semblants a aquesta”.
Com ho fan? Comparant els embeddings dels ítems: si dos ítems tenen vectors similars, és que els usuaris els valoren de manera semblant.
Com funciona?
En el collaborative filtering, cada element (una pel·lícula, un llibre, etc.) aprèn un conjunt de característiques x^{(i)}. Per exemple, en una explicació simplificada podríem imaginar que una característica indica si la pel·lícula és més aviat romàntica o d’acció.
A la pràctica, però, quan l’algorisme aprèn automàticament aquestes característiques, els valors x_1, x_2, x_3, \dots són difícils d’interpretar individualment. Pot ser complicat dir: “aquest component correspon a acció” i “aquest altre a cinema estranger”.
Tot i això, el conjunt de característiques sí que conté informació rellevant sobre el que defineix un ítem.
Per exemple, després d’entrenar amb les valoracions d’Alice, Bob, Carol i Dave, podríem tenir:
- Pel·lícula 1 (Love at Last): x^{(1)} = (1, 0)
- Pel·lícula 2 (Romance Forever): x^{(2)} = (0.98, 0)
- Pel·lícula 4 (Nonstop Car Chases): x^{(4)} = (0, 1)
Aquests valors són coherents amb els que hem deduït abans: les pel·lícules romàntiques tenen la primera component alta i les d’acció la segona.
Mesurant la semblança
Donades les característiques d’un ítem x^{(i)}, podem buscar-ne d’altres similars calculant la distància euclidiana entre vectors:
d(i, k) = \sqrt{\sum_{l=1}^{n} \left(x^{(k)}_l - x^{(i)}_l \right)^2}
O simplement la distància quadrada (més ràpida de calcular, mateix ordre):
d^2(i, k) = \sum_{l=1}^{n} \left(x^{(k)}_l - x^{(i)}_l \right)^2
Exemple concret:
Per trobar pel·lícules semblants a Love at Last (x^{(1)} = (1, 0)):
- Distància a Romance Forever: d^2 = (0.98-1)^2 + (0-0)^2 = 0.0004 → molt similar!
- Distància a Nonstop Car Chases: d^2 = (0-1)^2 + (1-0)^2 = 2 → molt diferents
Implementació pràctica:
Si en comptes de quedar-nos amb l’ítem més proper, seleccionem els 5 o 10 més propers, obtenim una llista de productes relacionats.
Algoritme:
- Calcular distàncies entre l’ítem i i tots els altres ítems
- Ordenar per distància creixent
- Retornar els K primers (excloent l’ítem mateix)
Optimització crítica: Aquestes similituds es poden precomputar offline:
- Cada nit, calcular les 20 pel·lícules més similars a cada pel·lícula
- Guardar en una taula de consulta ràpida
- Quan un usuari visita una pel·lícula, només cal consultar la taula (millisegons!)
Així, en un lloc web, podem recomanar productes semblants a aquell que l’usuari està consultant instantàniament, sense recalcular distàncies.
Tècniques avançades
Aquestes tècniques milloren el rendiment dels sistemes de recomanació, independentment de l’enfocament utilitzat (contingut o col·laboratiu).
Normalització de la mitjana
Aplicar normalització de la mitjana (mean normalization) no només accelera l’aprenentatge, sinó que també millora la qualitat de les prediccions, especialment per a usuaris nous.
El problema amb un usuari nou
Imaginem que afegim un nou usuari, l’Eva, que encara no ha valorat cap pel·lícula.
Si entrenem un sistema amb regularització, per a l’Eva obtindríem els paràmetres:
- w^{(5)} = [0,0]
- b^{(5)} = 0
Com que no hi ha valoracions seves, l’algorisme tendeix a portar w^{(5)} cap a zero. Això significa que les prediccions serien:
\hat{y}_{(5,i)} = w^{(5)} \cdot X(i) + b^{(5)} = 0
És a dir, l’algorisme prediu 0 estrelles a totes les pel·lícules. Això no és gaire útil.
Què és la normalització de la mitjana?
La idea és centrar les valoracions al voltant de la seva mitjana:
-
Calculem la mitjana \mu_i de cada pel·lícula i (tenint en compte només els usuaris que l’han valorat).
-
Construïm una nova matriu de valoracions Y’ restant aquestes mitjanes.
-
L’algorisme aprèn sobre Y’. La predicció per un usuari j i pel·lícula i és:
\hat{y}’_{(j,i)} = w^{(j)} \cdot x^{(i)} + b^{(j)}
A la predicció final cal tornar a afegir la mitjana:
\hat{y}_{(j,i)} = \hat{y}’_{(j,i)} + \mu_i
Per exemple, si l’Eva té w^{(5)} = [0,0] , b^{(5)} = 0 , per a una pel·lícula amb mitjana \mu_1 = 2.5 :
\hat{y}(5,1) = 0 + 2.5 = 2.5
Això és molt més raonable: per a un usuari nou, les primeres prediccions seran la mitjana de cada pel·lícula.
Beneficis
- Prediccions inicials més raonables per a usuaris amb poques o cap valoració.
- Millor comportament del sistema de recomanació, evitant prediccions trivials.
- L’optimització numèrica és més ràpida, perquè les dades estan centrades.
Feedback implícit
Fins ara hem treballat amb valoracions explícites (ratings): l’usuari expressa directament la seva opinió (1-5 estrelles, polze amunt/avall). Però en molts sistemes reals, aquest tipus de dades són escasses. La majoria d’usuaris no valoren el contingut que consumeixen.
Tipus de feedback
Existeixen dues categories principals:
Feedback explícit:
- L’usuari proporciona una valoració directa
- Exemples: estrelles (1-5), polze amunt/avall, puntuació (0-10)
- Avantatge: senyal molt clara de preferència
- Desavantatge: poques dades (la majoria d’usuaris no valoren)
Feedback implícit:
- Es dedueix del comportament de l’usuari
- Exemples:
- YouTube: va veure el vídeo? Quant de temps?
- Spotify: va escoltar la cançó sencera o la va saltar?
- Amazon: va clicar el producte? El va comprar?
- Netflix: va acabar la pel·lícula o la va abandonar?
- Avantatge: MOLTA més informació (tothom clica, pocs valoren)
- Desavantatge: senyal més sorollós (no clicar ≠ no m’agrada)
Implicació per al modelatge: El feedback implícit és inherentment binari (va clicar o no, va comprar o no). Per tant, els models amb feedback implícit són problemes de classificació binària (predir probabilitat d’interacció), mentre que els models amb feedback explícit són problemes de regressió (predir un valor numèric com 1-5 estrelles).
Quan és útil el feedback explícit?
El feedback explícit és especialment valuós quan:
- Necessitem saber la intensitat de la preferència: Un rating de 5 estrelles indica entusiasme; un 3 indica indiferència. El feedback implícit (clic/no clic) no captura aquests matisos.
- El cost d’un error és alt: En recomanacions mèdiques o financeres, és millor tenir poques dades però fiables que moltes dades sorolloses.
- L’usuari té motivació per valorar: Comunitats com Goodreads o Letterboxd funcionen perquè els usuaris volen expressar opinions.
Limitacions del feedback explícit:
- Biaix de selecció: Els usuaris tendeixen a valorar contingut que els ha agradat molt o molt poc, però no el contingut “normal”. Això distorsiona la distribució de ratings.
- Escassetat: En la majoria de plataformes, <1% dels usuaris valoren. Amb poques dades, els models són menys precisos.
- Inconsistència temporal: El mateix usuari pot donar valoracions diferents en moments diferents segons el seu estat d’ànim.
Conversió d’explícit a implícit
Quan tenim ratings explícits però volem usar tècniques de feedback implícit, apliquem un llindar (threshold):
\text{label}_{i,j} = \begin{cases} 1 & \text{si } y_{i,j} \geq \text{llindar} \quad \text{(interacció positiva)} \\ 0 & \text{si } y_{i,j} < \text{llindar} \quad \text{(interacció negativa)} \end{cases}
Exemple amb MovieLens:
- Si l’usuari dona ≥ 4 estrelles → label = 1 (li va agradar)
- Si l’usuari dona < 4 estrelles → label = 0 (no li va agradar)
Això converteix el problema de regressió (predir un valor numèric) en classificació binària (predir una probabilitat).
Per què convertir explícit a implícit?
Avantatges de la conversió:
- Simplicitat del model: La classificació binària és més senzilla que la regressió. Només cal predir “li agradarà?” en lloc de “quina nota posarà?”.
- Objectiu més alineat amb el negoci: Sovint l’important és si l’usuari consumirà el contingut, no la nota exacta que li posarà.
- Combinació de fonts de dades: Permet unificar ratings explícits amb senyals implícits (clics, visualitzacions) en un sol model.
- Robustesa al soroll: Les diferències entre un 3 i un 4 són subjectives; la distinció “li va agradar / no li va agradar” és més estable.
Inconvenients de la conversió:
- Pèrdua d’informació: Un 5 i un 4 es tracten igual, però l’usuari que dona un 5 probablement és més entusiasta.
- Elecció del llindar: Quin valor triar? 3.5? 4? La decisió és arbitrària i afecta els resultats.
- Distribució desbalancejada: Si la majoria de ratings són alts (com passa habitualment), tindrem molts més positius que negatius.
Quan fer la conversió?
| Situació | Recomanació |
|---|---|
| Vols predir si l’usuari consumirà un ítem | ✅ Convertir a implícit |
| Vols ordenar ítems per rellevància (ranking) | ✅ Convertir a implícit |
| Necessites predir la satisfacció exacta | ❌ Mantenir explícit |
| Tens molt poques dades explícites | ✅ Convertir i combinar amb implícit |
| El negoci requereix explicar “per què aquesta nota” | ❌ Mantenir explícit |
Model per feedback implícit
Amb feedback implícit, la predicció canvia:
Abans (ratings explícits): \hat{y}(i,j) = w^{(j)} \cdot x^{(i)} + b^{(j)} \quad \text{(valor entre 0 i 5)}
Ara (feedback implícit): p_{i,j} = \sigma(w^{(j)} \cdot x^{(i)} + b^{(j)}) \quad \text{(probabilitat entre 0 i 1)}
on \sigma és la funció sigmoide: \sigma(z) = \frac{1}{1 + e^{-z}}
Funció de cost
També canvia la funció de pèrdua:
- Ratings explícits: MSE (Mean Squared Error) o MAE
- Feedback implícit: Binary Cross-Entropy (BCE)
\text{BCE} = -\frac{1}{m} \sum_{(i,j) \in \text{observats}} \left[ y_{i,j} \log(p_{i,j}) + (1-y_{i,j}) \log(1-p_{i,j}) \right]
on y_{i,j} \in \{0, 1\} és el label real i p_{i,j} és la probabilitat predita.
Per què és important?
En sistemes de producció reals:
- YouTube: milers de milions de visualitzacions diàries, però poques valoracions amb polze
- Spotify: centenars de milions de reproduccions, però poques cançons “marcades com a favorites”
- Amazon: milers de milions de clics, però moltes menys compres i encara menys ressenyes
El feedback implícit multiplica per 100-1000× la quantitat de dades disponibles per entrenar el model.
Interpretació negativa
Precaució: El senyal negatiu és sorollós:
- No clicar un producte pot significar:
- No m’interessa (senyal útil)
- No l’he vist encara (no és senyal)
- Ja el tinc (no és senyal)
Per això, alguns sistemes només fan servir interaccions positives i ignoren les negatives, o les mostregen de manera especial.
Negative Sampling
Quan entrenem amb feedback implícit, tenim un problema: només tenim exemples positius (ítems que l’usuari ha clicat). Però per entrenar un classificador binari, necessitem també exemples negatius (ítems que no li interessen).
El problema
Una opció seria considerar tots els ítems no clicats com a negatius. Però això té dos problemes:
- Escala: Per cada clic, hi ha milions d’ítems no clicats. Calcular la pèrdua per tots és computacionalment inviable.
- Senyal: No clicar no significa “no m’agrada” — potser l’usuari simplement no ha vist l’ítem.
La solució: mostrejar negatius
En lloc de considerar tots els negatius, mostrejem aleatòriament uns pocs per cada positiu:
- Per cada parella positiva (usuari, ítem clicat), seleccionem K ítems aleatoris que l’usuari no ha clicat
- Entrenem el model per puntuar el positiu més alt que els K negatius
- Típicament, K ∈ [1, 20] segons el dataset
Exemple: L’usuari ha clicat la pel·lícula A. Mostrejem 4 pel·lícules aleatòries (B, C, D, E) que no ha clicat. El batch d’entrenament seria:
| Usuari | Ítem | Label |
|---|---|---|
| Alice | A | 1 (positiu) |
| Alice | B | 0 (negatiu) |
| Alice | C | 0 (negatiu) |
| Alice | D | 0 (negatiu) |
| Alice | E | 0 (negatiu) |
El model aprèn a donar una puntuació més alta a A que a B, C, D, E.
Per què funciona?
Intuïció: Si mostrejem ítems a l’atzar d’un catàleg de milions, la probabilitat que un ítem aleatori realment interessi a l’usuari és molt baixa. Per tant, els negatius aleatoris són “probablement correctes”.
Matemàticament: El negative sampling és una aproximació a la funció de pèrdua completa. Amb prou mostres aleatòries, el gradient estimat s’aproxima al gradient real, però amb un cost computacional molt menor.
En termes de rendiment:
- Sense negative sampling: O(milions) càlculs per cada positiu → entrenar un epoch triga dies
- Amb negative sampling (K=5): O(5) càlculs per cada positiu → entrenar un epoch triga minuts
Quants negatius mostrejar?
| K | Avantatge | Desavantatge |
|---|---|---|
| K=1 | Molt ràpid | Gradient sorollós |
| K=5-10 | Bon equilibri | Estàndard en la indústria |
| K=20+ | Gradient més estable | Més lent, rendiments decreixents |
YouTube i Spotify típicament usen K entre 5 i 20.
Negatius diferents cada epoch
Un detall important: els negatius es mostregen aleatòriament cada vegada que es recorre el dataset. Això significa que a cada epoch el model veu el mateix positiu (l’ítem que l’usuari va clicar) però amb negatius diferents.
Avantatge: Al llarg de l’entrenament, el model acaba veient una gran varietat de negatius per a cada positiu, millorant la seva capacitat de generalització. A més, aquesta variabilitat actua com una forma de regularització implícita que ajuda a evitar el sobreajustament.
Split temporal
En la majoria de problemes de ML, fem split aleatori: barregem les dades i agafem 80% train, 20% test. En recomanadors, això és un error greu.
Per què?
Les dades de recomanació tenen ordre temporal: l’usuari va clicar A el dilluns, B el dimarts, C el dimecres. Si fem split aleatori, el model pot “veure el futur” durant l’entrenament (saber que l’usuari va clicar C el dimecres) i haver de predir el passat (B el dimarts). Això és data leakage (fuita de dades) i dona mètriques falsament optimistes.
Data leakage: Es produeix quan, durant l’entrenament o l’avaluació d’un model, s’utilitza informació que en realitat no estaria disponible en el moment de fer prediccions reals, fet que provoca resultats artificialment bons.
La solució
Dividir les dades per temps, no aleatòriament:
- Train: totes les interaccions fins a una data X
- Test: totes les interaccions després de la data X
Així, el model només veu el passat i ha de predir el futur — exactament el que farà en producció.
Impacte
Un model avaluat amb split aleatori pot mostrar Precision@10 = 0.40, però amb split temporal baixa a 0.25. La diferència reflecteix el rendiment real en producció.
Com evolucionar el recomanador
Ara que hem vist els diferents enfocaments, podem comparar-los i entendre com evolucionar el sistema a mesura que creix.
| Aspecte | Basat en contingut | Col·laboratiu |
|---|---|---|
| Senyal principal | Característiques dels ítems | Comportament dels usuaris |
| Utilitza altres usuaris? | ❌ No | ✅ Sí |
| Funciona amb ítems nous? | ✅ Sí (usa metadades) | ❌ No (necessita valoracions) |
| Funciona amb usuaris nous? | ⚠️ Parcial (si té dades demogràfiques) | ❌ No (necessita historial) |
| Risc principal | Sobre-especialització | Cold start |
| Idea clau | “Contingut similar” | “Persones similars” |
Fases d’un projecte de recomanació
Un sistema de recomanació evoluciona amb el temps. A cada fase, les estratègies disponibles canvien segons les dades de què disposem.
Fase 1: Llançament (sense usuaris ni dades)
Quan el sistema és nou, no tenim valoracions. Les opcions són limitades:
- Predictors baseline: recomanar ítems populars o amb millor valoració externa (crítiques professionals, puntuacions d’altres plataformes)
- Basat en contingut: si tenim metadades dels ítems (gènere, any, actors…), podem recomanar ítems similars als que l’usuari indica que li agraden durant el registre
Fase 2: Primers usuaris (cold start d’usuaris)
A mesura que arriben usuaris, ens trobem amb el problema del cold start d’usuaris: un usuari nou no té historial.
- Usuaris nous: apliquem baselines (mitjana global \mu o mitjana per ítem \mu_i) combinats amb normalització de la mitjana per fer prediccions més raonables
- Usuaris amb algunes valoracions: podem començar a aprendre les seves preferències amb filtrat basat en contingut
Fase 3: Catàleg creixent (cold start d’ítems)
Quan el catàleg creix, apareix el cold start d’ítems: ítems nous sense valoracions.
- El filtrat col·laboratiu no pot recomanar ítems nous (no té dades)
- El basat en contingut sí que funciona: usa metadades (gènere, any, actors…) per trobar ítems similars
Fase 4: Sistema madur
Amb prou usuaris i valoracions, podem aprofitar tot el potencial:
- Filtrat col·laboratiu: alta precisió gràcies a patrons d’altres usuaris
- Sistemes híbrids: combinen col·laboratiu i contingut per cobrir tots els casos
| Fase | Dades disponibles | Estratègia recomanada |
|---|---|---|
| Llançament | Cap | Baselines, contingut (si hi ha metadades) |
| Primers usuaris | Pocs usuaris, algunes valoracions | Baselines + normalització mitjana |
| Catàleg creixent | Usuaris actius, ítems nous | Contingut per ítems nous, col·laboratiu per la resta |
| Sistema madur | Moltes dades | Híbrid (col·laboratiu + contingut) |
Sistemes híbrids: arquitectura two-tower
En un sistema madur, la millor estratègia és combinar col·laboratiu i contingut. L’arquitectura two-tower (dues torres) és la implementació dominant a la indústria (YouTube, Spotify, Pinterest, Airbnb…).
Estructura
Utilitzem dues xarxes neuronals independents que transformen les entrades en vectors de la mateixa dimensió:
- La torre d’usuari rep l’ID de l’usuari + característiques (edat, país, historial…) i produeix un vector v_u.
- La torre d’ítem rep l’ID de l’ítem + característiques (gènere, any…) i produeix un vector v_m.
- La predicció és el producte escalar v_u \cdot v_m.
Per què és híbrid?
Comparem els diferents enfocaments que hem vist:
| Enfocament | Entrada | Fortalesa | Debilitat |
|---|---|---|---|
| Filtrat basat en contingut | Només features | Funciona amb ítems nous | Limitat a similitud explícita |
| Factorització de matrius | Només IDs | Alta precisió amb dades | Cold start |
| Híbrid | IDs + features | Precisió + cold start | Més complex |
L’arquitectura two-tower és la manera més habitual d’implementar un sistema híbrid: combina l’ID (informació col·laborativa apresa de les interaccions) amb les característiques (informació de contingut) en una sola arquitectura.
Recordem que en la factorització de matrius, cada usuari i cada ítem tenen un embedding fix (una fila a la taula nn.Embedding). El problema: si arriba un usuari o ítem nou, no té embedding — és el cold start.
L’arquitectura two-tower resol això: les torres són xarxes neuronals que aprenen una funció que transforma característiques en embeddings (no una simple taula de cerca).
Exemple: Entrenem el model amb milers d’usuaris existents (amb les seves característiques i valoracions). Després arriba una usuària nova, Maria, que no ha valorat res encara.
- Factorització de matrius: L’ID de Maria no existeix a la taula d’embeddings → no podem fer prediccions.
- Two-tower: Sabem que Maria té 28 anys, viu a Espanya, usa l’app mòbil i ha navegat per sci-fi i documentals. La torre d’usuari (ja entrenada) transforma aquestes característiques en un embedding. Durant l’entrenament, la xarxa va aprendre que “usuaris de 28 anys que naveguen sci-fi en mòbil” tenen preferències similars a cert patró → podem recomanar des del primer moment.
La diferència clau: la factorització de matrius memoritza (un vector fix per cada ID vist durant l’entrenament), mentre que two-tower generalitza (aprèn una funció que pot generar vectors per a qualsevol combinació de característiques, incloent les no vistes).
Avantatges per escalabilitat
- Precomputació: Els vectors v_m de tots els ítems es poden calcular offline i guardar.
- Servei ràpid: Quan arriba un usuari, només cal calcular v_u una vegada i fer productes escalars amb els candidats.
- Cerca aproximada: Els vectors permeten usar algoritmes ANN (Approximate Nearest Neighbors) com FAISS o ScaNN per trobar els ítems més similars en mil·lisegons.
Escalabilitat
En els sistemes de recomanació actuals sovint cal seleccionar un petit conjunt d’ítems per recomanar, d’entre un catàleg que pot contenir milers, milions o fins i tot desenes de milions d’opcions. La pregunta clau és: com fer això de manera eficient computacionalment?
El repte computacional
Imaginem una xarxa neuronal que fem servir per predir la puntuació que un usuari donaria a un ítem. Un servei de vídeo en línia pot tenir milers de pel·lícules; una plataforma de publicitat, milions d’anuncis; un servei de música, desenes de milions de cançons; i una botiga en línia, milions de productes.
Quan un usuari entra a la web, disposem del seu vector de característiques X_u. Però si haguéssim de passar tots els milions d’ítems per la xarxa neuronal per calcular el producte intern amb X_u, el cost seria prohibitiu: fer inferència milions de vegades per cada visita és inviable.
Estratègia en dos passos
Per resoldre aquest problema, molts sistemes a gran escala utilitzen dos passos:
- Recuperació (retrieval): generar una llista inicial d’ítems candidats plausibles.
- Classificació (ranking): ordenar aquests candidats segons el model i escollir els millors.
Pas 1: Recuperació
L’objectiu és obtenir una llista prou àmplia i variada d’ítems que puguin ser interessants per a l’usuari. És acceptable que aquesta llista contingui molts ítems que l’usuari realment no voldrà.
Exemple d’estratègies de recuperació:
- Per cada una de les últimes 10 pel·lícules vistes, afegir les 10 pel·lícules més similars (calculades amb els embeddings x^{(i)} i x^{(k)}).
- Afegir les 10 pel·lícules més populars de cadascun dels tres gèneres preferits de l’usuari (per exemple, romàntic, comèdia i drama històric).
- Afegir les 20 pel·lícules més vistes al país de l’usuari.
Això es pot fer ràpidament si les similituds entre pel·lícules estan precomputades i accessibles en una taula de consulta. El resultat és una llista de centenars de candidats, amb bona cobertura i diversitat.
Finalment, es poden eliminar duplicats i ítems que l’usuari ja ha consumit.
Pas 2: Classificació
Ara treballem amb uns centenars de candidats. Per cada parella (usuari, ítem):
- Passem X_u (vector de l’usuari) i X_m (vector de l’ítem) pel model.
- Calculem la predicció \hat{y}(u,m), que pot ser la puntuació estimada o la probabilitat que l’usuari li doni una valoració positiva.
Això ens permet ordenar els ítems segons la seva rellevància prevista.
Optimització: Si hem precomputat els vectors V_m de tots els ítems, només cal calcular V_u una vegada per usuari. Després, la puntuació de cada candidat és un simple producte escalar — molt més ràpid que fer inferència completa sobre milions d’opcions.
En resum: els sistemes de recomanació a gran escala funcionen amb una estratègia de dos passos —recuperació i classificació— que els permet ser alhora ràpids i precisos, fins i tot amb catàlegs de desenes de milions d’ítems.
Oportunitats i riscos
Els sistemes de recomanació han estat molt profitosos per a moltes empreses. Tanmateix, alguns usos han tingut conseqüències negatives per a la societat. L’objectiu hauria de ser contribuir al benestar de les persones, no només generar benefici econòmic.
Engagement vs. benestar
Un sistema pot optimitzar diferents objectius:
- Recomanar pel·lícules que l’usuari valorarà bé → benefici per l’usuari
- Maximitzar temps de permanència → benefici per l’empresa (més anuncis)
- Mostrar productes amb més marge de benefici → benefici per l’empresa
El problema: maximitzar l’engagement (temps d’ús, clics) ha conduït algunes plataformes a amplificar contingut tòxic —conspiracions, odi, desinformació— perquè aquest contingut reté l’usuari. Filtrar-lo és difícil i controvertit.
Transparència
Molts usuaris pensen que les apps els recomanen allò que més els agradarà, però sovint el criteri és maximitzar el benefici de l’empresa. Fomentar la transparència —explicar amb quins criteris es decideixen les recomanacions— pot augmentar la confiança i reduir danys.
Responsabilitat
Quan dissenyem aquests sistemes, hem de pensar en els possibles danys, no només en els beneficis. La responsabilitat és col·lectiva: només construïm tecnologies que creiem que milloren la societat.
Metodologia Pràctica
- Introducció
- PART 1: COMENÇAR UN PROJECTE
- PART 2: SELECCIONAR I ENTRENAR MODELS
- PART 3: MILLORAR EL MODEL
- PART 4: CICLE COMPLET DE DESENVOLUPAMENT
- PART 5: TÈCNIQUES ESPECIALITZADES
- Ètica, Biaix i Justícia
- Resum dels principis clau
- Referències
Aquest document proporciona una guia de decisió pràctica per projectes de Machine Learning. Ofereix:
- Criteris de selecció d’algoritmes segons tipus de dades
- Estratègies de depuració i resolució de problemes
- Processos recomanats per diferents escenaris ML
- Guies de decisió basades en experiència pràctica
Complement: Per anàlisi matemàtica detallada amb fórmules, visualitzacions interactives i exemples de codi, consulta Guia d’aplicació i diagnòstic.
Introducció
Aplicar tècniques de machine learning amb èxit requereix més que només conèixer algoritmes. Cal saber:
- Quin algoritme triar per a cada problema
- Com interpretar els resultats dels experiments
- Què fer quan alguna cosa no funciona
Durant el desenvolupament quotidià, hauràs de prendre decisions com:
- Necessito més dades?
- El meu model és massa simple o massa complex?
- Com puc millorar el rendiment?
La bona notícia és que aplicar correctament un algoritme senzill sovint funciona millor que aplicar malament un algoritme complex.
Aquesta metodologia s’aplica tant a algoritmes clàssics (arbres de decisió, clustering, recomanadors) com a deep learning (xarxes neuronals). Els principis fonamentals són els mateixos, encara que les eines i tècniques específiques variïn.
Navegació ràpida
Per problemes específics
- 📊 Tinc dades tabulars (CSV/Excel) → Guia de selecció
- 🖼️ Treballo amb imatges/text/àudio → Guia de selecció i Transfer Learning
- 🔍 Vull fer clustering → Aprenentatge No Supervisat
- 💡 Vull fer un recomanador → Sistemes de Recomanació
- 🐛 El meu model no aprèn → Estratègies de depuració
- 📈 Necessito més dades? → Decidir si calen més dades
Per aprenentatge complet
Segueix les seccions en ordre per dominar la metodologia des dels fonaments fins a tècniques avançades.
PART 1: COMENÇAR UN PROJECTE
Procés de disseny
Segueix aquests passos:
-
Defineix els teus objectius - Quina mètrica d’error utilitzaràs? Quin valor objectiu vols assolir?
-
Crea un pipeline complet ràpidament - No perdis temps perfeccionant. Primer fes que funcioni d’extrem a extrem.
-
Identifica els coll d’ampolla - Diagnostica quines parts funcionen malament i per què.
-
Millora incrementalment - Fes canvis petits basats en el que has observat: més dades, ajustar paràmetres, canviar algoritmes…
Separació de dades: Train/Validation/Test
📝 Nota: Per detalls sobre la divisió de dades, validació creuada (K-Fold) i validació de models, consulta la Guia d’aplicació.
Abans de començar, separa les teves dades en tres conjunts:
- Entrenament (60-80%): Per entrenar el model i ajustar els pesos
- Validació (10-20%): Per ajustar hiperparàmetres i decidir quan aturar l’entrenament
- Test (10-20%): Per avaluar el rendiment final del model
Regla d’or: Mai entrenis amb dades de test. Són sagrades i només s’utilitzen al final per mesurar el rendiment real.
Conceptes clau: Underfitting i Overfitting
📝 Nota: Per una anàlisi matemàtica detallada de biaix i variància, consulta la Guia d’aplicació.
Dos problemes fonamentals que trobem sempre en machine learning:
-
Underfitting (subajust): El model és massa simple i no captura els patrons importants de les dades. Error alt tant en entrenament com en test/validació.
-
Overfitting (sobreajust): El model memoritza els exemples d’entrenament incloent el soroll, però no generalitza a dades noves. Error baix en entrenament, error alt en test/validació.
-
Model correcte: Captura els patrons reals i generalitza bé a dades noves. Error acceptable tant en entrenament com en test.
Diagnòstic ràpid:
- Error train ALT + Error test ALT → Underfitting (model massa simple)
- Error train BAIX + Error test ALT → Overfitting (model massa complex o poques dades)
- Error train BAIX + Error test BAIX → Model correcte! ✅
Mètriques de rendiment
Per què són importants?
La mètrica que escullis guiarà totes les teves decisions futures. És impossible assolir error zero perquè:
- Les dades d’entrada poden no tenir tota la informació necessària
- Només tens una quantitat finita de dades d’entrenament
- El sistema pot ser intrínsecament probabilístic
Per a triar una bona mètrica, considera què necessita l’aplicació per ser útil, segura o atractiva.
Mètriques comunes
Precisió/Error: La més bàsica, però no sempre la millor.
Precisió i Recall: Útils quan tens classes desbalancejades.
-
Precision: De les deteccions que el model ha fet, quantes són correctes? \text{Precision} = \frac{\text{Deteccions correctes}}{\text{Total de deteccions}}
-
Recall: De tots els casos reals, quants ha detectat el model? \text{Recall} = \frac{\text{Deteccions correctes}}{\text{Total de casos reals}}
Exemple: Imagina un test mèdic per una malaltia rara (1 de cada milió de persones):
- Si el model sempre diu “no té la malaltia”, té 99.9999% de precisió però és inútil
- Necessitem precision i recall per avaluar-lo correctament
F-score: Combina precision i recall en un sol número:
F = \frac{2 \times \text{precision} \times \text{recall}}{\text{precision} + \text{recall}}
Cobertura: Fracció d’exemples que el model pot processar (pot refusar-se quan no està segur).
PART 2: SELECCIONAR I ENTRENAR MODELS
Models base (Baseline)
Aquesta secció es centra en aprenentatge supervisat, el cas més comú (classificació i regressió amb dades etiquetades).
📝 Nota: Si treballes amb clustering, reducció de dimensionalitat o recomanadors, consulta les seccions específiques més endavant.
Guia de selecció d’algoritme
Comença aquí si vols una resposta ràpida! Aquesta és una guia simplificada per als casos més comuns.
💡 Baseline trivial: Abans de res, prova sempre un baseline mínim (classe majoritària per classificació, mitjana per regressió) per tenir un punt de referència.
| Tipus de dades | Primera opció (comença aquí) | Segona opció (si la primera no funciona) |
|---|---|---|
| Dades tabulars (poques features, <50) | Regressió Logística / Regressió Lineal | Arbres de decisió (Random Forest, Gradient Boosting) |
| Dades tabulars (moltes features, >50) | Arbres de decisió (Random Forest, Gradient Boosting) | Xarxes neuronals (si tens moltes dades) |
| Text | TF-IDF + Regressió Logística (ràpid) | Transformer preentrenat (millor qualitat) |
| Imatges | CNN o ViT preentrenat (xarxa neuronal) | Fine-tuning amb models més grans |
| Àudio | Model preentrenat especialitzat | Fine-tuning o CNN sobre espectrogrames |
| Sèries temporals | Models estadístics clàssics (ARIMA, Prophet) | Gradient boosting amb features temporals |
Per què aquestes recomanacions?
-
Dades tabulars (<50 features): Regressió Logística/Lineal és simple, ràpid, interpretable. Si no funciona → Arbres de decisió.
-
Dades tabulars (>50 features): Arbres de decisió capturen relacions no lineals millor que regressió, i sovint superen xarxes neuronals.
-
Text: TF-IDF + Regressió és sorprenentment efectiu i ràpid (ideal per prototips). Transformers són millors però requereixen GPU.
-
Imatges/Àudio: Xarxes neuronals preentrenades SÓN la primera opció. Molt superiors a algoritmes clàssics. Sempre usa transfer learning.
-
Sèries temporals: Models estadístics clàssics són baselines ràpids i efectius. Per problemes complexos amb múltiples variables o patrons no lineals, considera xarxes recurrents (LSTM/GRU) o Transformers temporals.
Regles d’or:
- El tipus de dades determina l’algoritme: Imatges/Àudio → Xarxes neuronals SEMPRE. Text → Depèn dels recursos. Tabulars → Comença simple.
- Ordre de complexitat per dades tabulars: Regressió → Random Forest → Gradient Boosting → Neural nets
- Imatges/Text/Àudio: Sempre usa models preentrenats (transfer learning). Mai entrenis des de zero.
- Poc temps/recursos: Usa opcions ràpides de la taula (TF-IDF per text, models petits preentrenats per imatges).
Eines recomanades:
- Llibreries de ML clàssic (ex. Scikit-learn): Models basats en arbres, regressió, SVM
- Llibreries de gradient boosting (ex. XGBoost, LightGBM, CatBoost): Excel·lent per dades tabulars
- Frameworks de transformers (ex. HuggingFace): Models preentrenats per text, imatges i àudio
Començar ràpidament: Configuracions per defecte
La millor notícia per principiants: Les llibreries modernes venen amb configuracions per defecte excel·lents. No cal ajustar gaire per començar!
Models lineals (Regressió Logística/Lineal)
Avantatges: ⚡ Ràpid, 📊 Interpretable, 🎯 Excel·lent baseline
Quan passar a models més complexos: Si l’error és alt i sospites relacions no lineals → Arbres de decisió o gradient boosting
Models basats en arbres (Random Forest, Gradient Boosting)
Avantatges: 🌳 Capta relacions no lineals, 🎯 Robust a outliers, 📈 Excel·lent per dades tabulars
Consell: Comença amb els valors per defecte. Només ajusta si veus overfitting (redueix complexitat) o underfitting (augmenta complexitat).
Deep learning (xarxes neuronals)
Avantatges: 🖼️ Excel·lent per imatges/text/àudio, 🧠 Aprèn representacions complexes, 🚀 Escalable amb més dades
Regla simple: Usa optimitzadors adaptatius (Adam, AdamW) i funcions d’activació estàndard (ReLU per CNNs). Els frameworks moderns ja ho fan per defecte.
Regularització essencial:
- Early stopping: Atura l’entrenament automàticament quan la validació deixa de millorar (evita overfitting)
- Dropout (0.3-0.5): Desactiva neurones aleatòriament durant l’entrenament per millorar la generalització
Transfer Learning: Aprofita models preentrenats
Regla d’or: No sempre cal entrenament des de zero. Reutilitza models preentrenats!
Què és? Començar amb un model que ja ha après d’un gran dataset i adaptar-lo al teu problema (fine-tuning).
Per què funciona tan bé?
- El model ja ha après característiques útils (formes, colors, patrons de text, etc.)
- Necessites moltes menys dades per entrenar
- Convergeix molt més ràpid
On trobar models preentrenats:
- Hubs de models: Repositoris públics amb models de NLP, visió i àudio (ex. HuggingFace Hub)
- Repositoris oficials: Models oficials de frameworks (TensorFlow/PyTorch Hub)
Recomanació pràctica:
- Per text: Utilitza models Transformer preentrenats (BERT, GPT, RoBERTa)
- Per imatges: Utilitza CNNs o Vision Transformers preentrenats (ResNet, EfficientNet, ViT)
- Només entrena des de zero si tens un problema molt específic i milions de dades
Estratègies d’aplicació:
- Feature extraction: Congela el model preentrenat, només entrena la capa final. Útil amb poques dades (<1000 exemples).
- Fine-tuning: Descongela algunes o totes les capes i reentrena amb learning rate baix. Útil amb més dades (>5000 exemples).
- Gradual unfreezing: Primer entrena només la capa final, després descongela progressivament més capes. Evita “catastrophic forgetting”.
💡 Resum ràpid - Models base:
- Dades tabulars simples (poques features) → Regressió Logística/Lineal primer, després arbres de decisió
- Dades tabulars complexes (moltes features) → Random Forest o Gradient Boosting
- Imatges/Text/Àudio → Models preentrenats amb transfer learning
- Comença pel més simple: Baseline simple millor que model complex mal entrenat
- No reinventis la roda: Utilitza llibreries estàndard i models preentrenats
PART 3: MILLORAR EL MODEL
Decidir si calen més dades
Molts principiants proven algoritmes diferents quan sovint el problema és la quantitat o qualitat de les dades.
Diagnostic: Com decidir?
-
Comprova el rendiment en entrenament
Si l’error d’entrenament és alt:
- ❌ NO recullis més dades encara
- ✅ Augmenta la mida del model (més capes, més neurones)
- ✅ Millora l’algoritme d’aprenentatge (ajusta learning rate)
- ⚠️ Potser les dades són massa sorolloses o no tenen la informació necessària
Si l’error d’entrenament és acceptable:
- ➡️ Passa al següent pas
-
Comprova el rendiment en test
Si l’error de test és acceptable:
- 🎉 Has acabat!
Si l’error de test és molt més alt que el d’entrenament:
- ✅ Recull més dades (sovint la millor solució)
- ✅ Prova data augmentation (més ràpid que recollir dades)
- ✅ Afegeix regularització (dropout, weight decay)
- ✅ Redueix la mida del model
Data Augmentation: Augmentar artificialment les dades
Abans de recollir més dades reals, pots augmentar artificialment el teu dataset existent.
Tècniques bàsiques
Imatges: Rotacions, flips, retalls aleatoris, ajustos de color/brillantor
Text: Synonym replacement, back-translation
Àudio: Time stretching, pitch shifting, afegir soroll de fons
Tècniques avançades
Imatges:
- Mixup: Combinar dues imatges amb pesos aleatoris
- CutMix: Retalls superposats d’imatges diferents
- RandAugment: Transformacions aleatòries amb magnituds automàtiques
Text:
- Paraphrase generation: Models generatius (T5, PEGASUS) per crear variants semàntiques
Àudio:
- SpecAugment: Màscara de bandes de freqüència/temps en espectrogrames
Quan utilitzar data augmentation
- ✅ Tens poques dades (centenars o milers d’exemples)
- ✅ El model sobreajusta (gran diferència entre train i test)
- ✅ Recollir més dades és car o lent
- ❌ Ja tens milions d’exemples (l’impacte és menor)
Impacte esperat: Pot duplicar o triplicar l’efectivitat del teu dataset.
Important: Assegura’t que les dades augmentades siguin representatives del conjunt de prova.
Si decideixes recollir més dades: Quantes?
Comença amb una estratègia simple:
- Duplica la quantitat de dades que tens
- Si veus millora significativa, continua recollint més
- Si la millora és petita, potser el problema és el model, no les dades
Regla pràctica:
- Amb centenars d’exemples: Prova arribar a milers
- Amb milers: Prova arribar a desenes de milers
- Si ja tens moltes dades i no millora: El problema probablement no és la quantitat de dades
📝 Nota: Pots utilitzar learning curves per estimar quantes dades necessites: entrena amb diferents quantitats (100, 200, 400, 800…), grafica l’error vs. mida del dataset, i extrapola.
Ajustar Hiperparàmetres
Què són els hiperparàmetres?
Configuracions de l’algoritme que afecten el temps d’entrenament i la qualitat del model final.
L’hiperparàmetre més important: Learning Rate
El learning rate és el més crític. Si només pots ajustar un, ajusta aquest:
- Massa gran → L’entrenament és inestable, la loss salta
- Massa petit → L’entrenament és molt lent
- Òptim → Convergència ràpida i estable
Com trobar-lo: Comença amb 0.001 (o el valor per defecte de l’optimitzador) i ajusta:
- Si la loss no baixa → Prova 10× més gran
- Si la loss salta molt → Prova 10× més petit
Estratègia recomanada per principiants
- Comença amb valors per defecte - Els frameworks moderns tenen configuracions que funcionen bé
- Ajusta NOMÉS el learning rate si veus problemes (el més crític!)
- Ajusta la mida del model (més/menys neurones) segons underfitting/overfitting
- No perdis massa temps optimitzant - sovint més dades ajuda més que hiperparàmetres perfectes
Optimització automàtica d’hiperparàmetres
Si necessites optimitzar múltiples hiperparàmetres sistemàticament:
Grid Search: Prova totes les combinacions possibles (exhaustiu però costós)
Random Search: Mostra valors aleatòriament (més eficient, recomanat com a primer pas)
Optimització Bayesiana: Construeix un model per trobar valors òptims amb menys experiments (per ús avançat)
Eines: Llibreries especialitzades (Optuna, Ray Tune, Weights & Biases Sweeps)
Recomanació: Comença amb ajust manual per entendre els hiperparàmetres, després prova Random Search si cal.
Estratègies de depuració
Els sistemes de machine learning són difícils de depurar perquè:
- No sabem quin hauria de ser el comportament exacte
- Diferents parts del model poden compensar-se mútuament
Flowchart ràpid: “El model no aprèn”
| Símptoma | Diagnostic | Solució |
|---|---|---|
| 🔴 Error train ALT | Underfitting / Bug | → Augmenta capacitat (més neurones/capes) → Verifica learning rate (massa baix?) → Prova amb 1-10 exemples per detectar bugs |
| 🟡 Error train OK, test ALT | Overfitting | → Data augmentation → Més dades reals → Regularització (dropout, weight decay) → Early stopping |
| 🟠 Error train i test ALTS | Underfitting + Possibles problemes | → Augmenta capacitat → Verifica qualitat de dades → Comprova si hi ha bugs |
| 🔵 Loss no baixa / NaN | Problema d’optimització | → Redueix learning rate (prova 0.1×) → Verifica gradient exploding → Comprova normalització de dades |
| 🟢 Entrenar perfecte, test aleatori | Overfitting extrem / Bug avaluació | → Verifica data leakage → Comprova que train/test són diferents → Revisa preprocessament |
Tests de depuració importants
-
Visualitza el model en acció
- Imatges: Mostra les deteccions superposades
- Àudio: Escolta les mostres generades
- No et limitis a mirar només números!
-
Visualitza els pitjors errors
- Ordena per confiança i mira els errors més confiats
- Sovint revelen problemes sistemàtics amb les dades
-
Raona amb error d’entrenament vs. test
Situació Interpretació Error entrenament baix, error test alt Probablement overfitting (o error en l’avaluació) Error entrenament alt, error test alt Pot ser underfitting o un bug de software -
Ajusta un dataset molt petit
- Si no pots ajustar ni 1 exemple, hi ha un bug
- Prova amb 1-10 exemples per verificar que l’optimització funciona
-
Monitoritza l’entrenament
- Comprova que la loss baixa gradualment (si es queda constant o puja, hi ha un problema)
- Verifica que els gradients no exploten (valors NaN o Inf) ni desapareixen (loss no canvia)
- Utilitza eines de visualització (TensorBoard, Weights & Biases) per seguir mètriques en temps real
PART 4: CICLE COMPLET DE DESENVOLUPAMENT
Cicle de desenvolupament d’un projecte
Un projecte complet de Machine Learning segueix aquestes fases:
- Definir el projecte - Objectius, mètriques, criteris d’èxit
- Definir i recollir les dades - Identificar fonts, etiquetar, validar qualitat
- Entrenar el model (procés iteratiu)
- Escollir l’arquitectura (model, dades, etc.)
- Entrenar el model
- Diagnosticar (biaix, variància, anàlisi de l’error)
- Desplegar el projecte - Producció, monitoratge, manteniment
📝 Nota: Per diagnòstic detallat amb fórmules matemàtiques, consulta Guia d’aplicació i diagnòstic.
Anàlisi de l’error
Més enllà del diagnòstic de biaix i variància, l’anàlisi qualitativa dels errors és crucial per millorar el model.
Procediment recomanat:
Suposem que tenim un conjunt de validació amb 500 exemples i 100 classificacions errònies.
-
Examinar manualment les mostres mal classificades i categoritzar-les segons trets comuns
- Les categories poden solapar-se
- Si hi ha massa errors, selecciona un subconjunt aleatori
-
Identificar patrons d’errors freqüents amb gran impacte:
- Recollir més dades d’aquesta categoria
- Crear noves característiques relacionades amb aquests casos
- Implementar filtres o preprocessament específic
Quan és útil aquest mètode:
- ✅ Problemes que els humans resolen bé (ex. detecció de spam, reconeixement d’imatges)
- ❌ Problemes on els humans no tenen intuïció (ex. predir clics en anuncis)
Estratègies per afegir dades
L’enfocament centrat en les dades sovint té més impacte que canviar de model. Diversos models poden funcionar bé amb bones dades, però cap model funciona bé amb dades insuficients.
Recollida de noves dades
- Ampliar el dataset pot ser lent i costós, però és la via més directa
- Recollida dirigida: Centra’t en categories identificades en l’anàlisi d’errors
- Dades no etiquetades: Recull primer, etiqueta després segons necessitats
Augment de dades
Veure secció detallada a Data Augmentation.
Tècniques principals:
- Imatges: Rotacions, flips, retalls, ajustos de color, Mixup, CutMix, RandAugment
- Text: Synonym replacement, back-translation, paraphrase generation
- Àudio: Time stretching, pitch shifting, soroll de fons, SpecAugment
Important: Assegura’t que les dades augmentades siguin representatives del conjunt de prova.
Síntesi de dades
Generar exemples nous mitjançant entrades artificials:
- Sobretot utilitzat en visió per computador
- Exemple: Generar imatges de text amb diferents fonts per entrenar OCR
- Útil quan recollir dades reals és complicat o costós
Resum: Fes créixer el dataset intel·ligentment combinant dades reals, augmentació i síntesi segons el problema.
Desplegament i MLOps
El desplegament consisteix a implementar un servidor d’inferència que integra el model en producció.
Components clau:
-
API d’inferència
- Rep dades d’entrada (ex. imatge, text, àudio)
- Retorna prediccions del model
- Ha de garantir prediccions fiables i eficients
-
Logging i monitoratge
- Registra totes les inferències
- Monitoritza qualitat de prediccions
- Detecta degradació del model (data drift)
-
Reentrenament
- Les dades noves poden millorar el model
- Cicle de reentrenament periòdic
- Validació abans de desplegar noves versions
MLOps (Machine Learning Operations) inclou:
- Gestió de dades i models
- Control de versions (codi + dades + models)
- Monitoratge en producció
- Reentrenament automàtic
- Escalabilitat i alta disponibilitat
PART 5: TÈCNIQUES ESPECIALITZADES
Fins aquí, hem cobert els principis fonamentals que resolen el 90% dels casos reals en machine learning. Si has arribat a aquest punt, ja tens les eines per construir models efectius.
Aquesta secció presenta breument tècniques especialitzades per a problemes més específics.
⚠️ Nota per principiants: Aquestes són tècniques avançades. No cal dominar-les per començar - primer assegura’t de comprendre bé els conceptes de les seccions anteriors.
Aprenentatge No Supervisat
L’aprenentatge no supervisat treballa amb dades sense etiquetes. Els tres tipus principals:
1. Clustering (agrupar dades similars)
- Algoritmes: K-means (primera opció), DBSCAN (formes irregulars), Hierarchical (jerarquies)
- Preprocessament crític: Normalitza sempre les dades (StandardScaler, MinMaxScaler)
- Triar K: Utilitza Elbow Method, Silhouette Score, o domain knowledge
- Validació: Visualitza clusters (PCA/UMAP), analitza mètriques (Silhouette Score, Davies-Bouldin Index)
- Aplicacions: Segmentació de clients, detecció de grups naturals
2. Reducció de dimensionalitat (simplificar dades mantenint informació)
- PCA: Preprocessament, reducció de features (escull components que expliquin 95% variància)
- UMAP/t-SNE: Visualització (reduir a 2D/3D per graficar), NO per preprocessament
- Aplicacions: Visualització, preprocessament abans de supervisat
3. Detecció d’anomalies (identificar casos atípics)
- Algoritmes: Isolation Forest, One-Class SVM, Autoencoders
- Aplicacions: Detecció de frau, defectes de fabricació, intrusió
Reptes clau:
- No tens ground truth per validar
- Cal validar qualitativament i amb mètriques específiques
- Els resultats depenen molt del preprocessament
Sistemes de Recomanació
Els recomanadors tenen reptes únics que requereixen mètriques i tècniques específiques.
Tipus de recomanadors:
- Collaborative Filtering: Basats en similitud entre usuaris/ítems (ex. “usuaris com tu van gaudir de X”)
- Content-Based: Basats en característiques dels ítems (ex. “aquest té característiques similars a Y”)
- Híbrids: Combinen ambdós (millor opció en producció, ex. Netflix, Spotify)
Metodologia:
- Comença simple: Matrix Factorization (SVD, ALS) o similitud cosinus
- Mètriques específiques: NDCG@K, Precision@K, Recall@K, Coverage, Diversity (NO accuracy/MSE!)
- Validació temporal: Train/test split temporal (NO aleatori) - prediu el futur amb dades passades
- Cold Start: Estratègia híbrida per usuaris/ítems nous
- A/B testing: Imprescindible en producció (mètriques offline no capturen tot)
Problemes comuns:
- Recomanar sempre els mateixos ítems populars → Afegeix penalització per popularitat
- No funciona per usuaris nous → Estratègia híbrida o onboarding
- Mètriques bones però usuaris insatisfets → Cal A/B testing i engagement real
Casos especialitzats de dades
Dades d’alta dimensionalitat (>1000 features):
- Xarxes neuronals profundes o regressió amb regularització L1/ElasticNet
Dades multimodals (imatge+text):
- Models especialitzats (CLIP, ViLT, BLIP) o fusió de models separats
Vídeo:
- Vision Transformers temporals o CNN 3D
Text llarg (>512 tokens):
- Transformers especialitzats (Longformer, BigBird) o hierarchical transformers
Grafos (xarxes socials, molècules):
- Graph Neural Networks (GCN, GAT)
Embeddings i cerca semàntica:
- Models d’embeddings (Sentence-BERT, CLIP) + Vector databases (Pinecone, Weaviate, Chroma)
- Aplicacions: RAG (Retrieval-Augmented Generation), cerca semàntica, classificació escalable
Tècniques avançades d’entrenament
Learning rate scheduling:
- Warmup: Comença amb lr baix, augmenta gradualment (estabilitza entrenament inicial)
- Cosine annealing: Redueix lr seguint corba cosinus (millora convergència final)
Gradient clipping: Limita magnitud de gradients per prevenir explosions (essencial per Transformers/RNNs)
Fine-tuning eficient (per models molt grans):
- LoRA/QLoRA: Ajusta només una petita part dels paràmetres (menys memòria GPU)
- Útil per LLMs de 7B+ paràmetres en GPUs limitades
Normalització:
- Batch Normalization: Per CNNs (normalitza sobre batch)
- Layer Normalization: Per Transformers (normalitza sobre features, més robust)
Ètica, Biaix i Justícia
Més enllà de l’excel·lència tècnica: Desenvolupar models de machine learning comporta una responsabilitat social important. Aquesta secció complementa la metodologia tècnica anterior amb consideracions ètiques essencials que han d’estar presents en totes les fases del desenvolupament.
Els algoritmes d’aprenentatge automàtic afecten avui milions de persones. Per això, és imprescindible reflexionar sobre si els sistemes que desenvolupem són:
- raonablement justos,
- lliures de biaixos,
- i èticament acceptables.
Exemples reals de problemes
En la història de l’aprenentatge automàtic, hi ha hagut sistemes amb un nivell de biaix inacceptable. Alguns casos coneguts:
- Contractació: una eina de selecció de personal discriminava contra les dones.
- Reconeixement facial: alguns sistemes identificaven persones amb pell fosca com a delinqüents amb molta més freqüència que les de pell clara.
- Concessió de préstecs: algorismes bancaris que aprovaven crèdits de manera discriminatòria segons el subgrup social.
- Estereotips: motors de cerca que reforcen rols de gènere o exclouen col·lectius de determinades professions.
- Deepfakes: vídeos generats artificialment sense consentiment ni transparència.
- Xarxes socials: difusió de discurs d’odi o incendiari, afavorit per algorismes que optimitzen per engagement.
- Fraus i bots: ús d’IA per crear contingut fals, manipular opinions o cometre delictes financers.
Aquests casos mostren que els errors en dissenyar i desplegar sistemes d’IA poden tenir conseqüències greus i socials.
Principis generals
-
No construir sistemes amb impacte negatiu Si un projecte és clarament nociu per la societat, la decisió ètica és no fer-lo. (Molts investigadors i enginyers han rebutjat projectes tot i ser rendibles).
-
La complexitat de l’ètica L’ètica és un camp antic i complex: no hi ha una “llista curta de 5 punts” per garantir decisions correctes. El millor que podem fer és establir bones pràctiques i reflexió crítica col·lectiva.
Bones pràctiques per reduir biaix i ser més justos
-
Formar equips diversos
- Diversitat de gènere, ètnia, cultura i experiència.
- Els equips diversos són més capaços de detectar possibles problemes i d’anticipar riscos.
-
Revisar literatura i estàndards
- Consultar normatives emergents en cada sector (ex. finances, salut).
- Aplicar guies i bones pràctiques establertes.
-
Auditories de biaix abans del desplegament
- Després d’entrenar un model, avaluar el seu rendiment per subgrups (gènere, ètnia, edat, etc.).
- Identificar si hi ha discriminació sistemàtica i corregir-la abans de desplegar-lo.
-
Plans de mitigació
- Preparar estratègies de resposta en cas de problemes:
- ex. tornar a un model anterior més segur,
- Desplegar ràpidament una versió corregida.
- ex. equips de cotxes autònoms defineixen protocols clars en cas d’accidents.
-
Monitoratge després del desplegament
- Continuar supervisant l’impacte del sistema un cop en producció.
- Detectar ràpidament efectes nocius i actuar.
Impacte relatiu dels projectes
No tots els projectes tenen el mateix pes ètic:
- Exemple trivial: una xarxa neuronal que decideix quant temps torrar un cafè.
- Exemple crític: un algorisme que decideix qui rep un préstec o accés a atenció mèdica.
Per això, cal avaluar el risc social abans de desenvolupar i desplegar qualsevol sistema.
Resum
- Els sistemes de ML poden tenir un gran impacte en la societat.
- La responsabilitat dels equips és detectar, evitar i mitigar biaixos i injustícies.
- No hem de repetir errors del passat: la comunitat ha de continuar millorant en la gestió de l’ètica i la justícia en IA.
- Aquest tema és seriós, complex i crucial: el futur de la tecnologia depèn de fer-ho bé.
Resum dels principis clau
Essencials per començar (tots els tipus de ML)
- Identifica el tipus de problema - Supervisat, no supervisat o recomanació?
- Defineix objectius clars - Tria la mètrica adequada des del principi
- Comença simple - Models lineals/arbres per tabulars, models preentrenats per imatges/text
- Diagnostica amb dades - Error d’entrenament vs. test (per supervisat), mètriques específiques per altres
- Visualitza - Mira què fa el model, no només els números
- Millora incrementalment - Canvis petits basats en observacions
Per aprenentatge supervisat
- Dades tabulars → Prova regressió o arbres de decisió abans de xarxes neuronals
- “IA completa” (imatges, text, àudio) → Deep learning amb transfer learning
- Més dades sovint millora més que més complexitat
- Learning rate és l’hiperparàmetre més important (per neural networks)
- Regularització (early stopping, dropout) abans d’afegir més dades
Per aprenentatge no supervisat
- Normalitza les dades - Crucial per clustering i PCA
- No hi ha una resposta correcta - Valida qualitativament amb visualitzacions
- Elbow method i Silhouette per triar K en clustering
- K-means primer - Si no funciona, prova DBSCAN
- PCA per preprocessament, UMAP per visualització
Per sistemes de recomanació
- Comença amb Matrix Factorization (SVD) - Simple i efectiu
- Utilitza temporal split per validar, NO split aleatori
- Mètriques: NDCG@K, Precision@K (NO accuracy/MSE)
- Cold start: Necessites estratègia híbrida o onboarding
- A/B testing en producció - Les mètriques offline no ho diuen tot
Quan tinguis més experiència
- Transfer learning per defecte (deep learning) - Gairebé mai entrenis des de zero
- Les tècniques avançades són útils però no imprescindibles per començar
- Domain knowledge sovint és més valuós que algoritmes complexos
La clau de l’èxit: Aplicar correctament la metodologia bàsica amb algoritmes estàndard és millor que utilitzar tècniques complexes sense entendre-les!
Referències
Desplegament i operació
Tot el que cal saber per portar models de machine learning a producció i mantenir-los funcionant correctament al llarg del temps.
- Desplegament de models: MLOps, Docker, APIs i versionat per portar models a producció de manera fiable.
- Validació i qualitat: Criteris de validació, testing específic per ML i pipelines CI/CD per garantir la qualitat dels models.
- Drifting i monitoratge: Detecció de canvis en la distribució de dades i sistemes d’alertes per models en producció.
- Aprenentatge continu: Estratègies per actualitzar i re-entrenar models quan les dades canvien.
- Patrons d’implementació: Receptes pràctiques per resoldre problemes concrets quan es porta un model de ML a producció.
Desplegament de models
- Què és MLOps?
- El pipeline: la unitat central d’MLOps
- Per què el desplegament és difícil?
- Preparació de dades per producció
- Serialització de models
- Predicció online vs. predicció batch
- Servir models amb FastAPI
- Contenidors amb Docker
- CI/CD: Integració i desplegament continus
- Resum
Què és MLOps?
MLOps (Machine Learning Operations) és la disciplina que combina Machine Learning, DevOps i enginyeria de dades per automatitzar i millorar tot el cicle de vida dels models de ML en producció.
Mentre que el desenvolupament de models se centra en entrenar algoritmes precisos, MLOps se centra en:
- Desplegar models de manera fiable i reproducible
- Monitoritzar el comportament dels models en producció
- Automatitzar el procés d’actualització i reentrenament
- Garantir la qualitat amb testing i validació contínua
El cicle de vida MLOps
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Develop │ → │ Deploy │ → │ Monitor │ → │ Update │
│ & Train │ │ & Serve │ │ & Alert │ │ & Retrain │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
Cap. 1 Cap. 1-2 Cap. 3 Cap. 4
En aquest mòdul explorarem els quatre pilars fonamentals de MLOps:
- Desplegament de models (aquest capítol): Docker, APIs, versionat
- Qualitat i testing: Validació, CI/CD, estratègies de desplegament
- Monitorització de drift: Detecció de degradació, alertes
- Aprenentatge continu: Reentrenament, A/B testing, rollback
Però abans d’entrar en detall, hi ha un concepte que connecta tots quatre pilars i que és fonamental per entendre MLOps.
El pipeline: la unitat central d’MLOps
En MLOps no despleguem només un model — despleguem un pipeline complet. Un pipeline és una seqüència d’etapes connectades on la sortida d’una etapa és l’entrada de la següent, dissenyat per executar-se de manera automatitzada i reproduïble. El cicle MLOps complet inclou diversos sub-pipelines especialitzats, cadascun amb la seva responsabilitat:
┌─► Dades d'entrada (Data sources)
│ │ · Primera execució: dataset original
│ │ · Reentrenament: + dades de producció (logs)
│ │
│ ▼
│ ┌────────────────────────────┐
│ │ Preprocessament │
│ │ (Data pipeline) │ Validar, netejar, split, Parquet
│ └────────┬───────────────────┘
│ │
│ ▼
│ ┌────────────────────────────┐
│ │ Entrenament │
│ │ (Training pipeline) │ Train → model + preprocessors
│ └────────┬───────────────────┘
│ │
│ ▼
│ ┌────────────────────────────┐
│ │ Validació │
│ │ (Quality gate) │ F1 > llindar? compare prev.
│ └────────┬───────────────────┘
│ │
│ ├── ✗ No → STOP
│ │
│ ▼
│ ┌────────────────────────────┐
│ │ Desplegament │
│ │ (Servei / CI-CD) │ Docker + FastAPI + versió
│ └────────┬───────────────────┘
│ │
│ ▼
│ ┌────────────────────────────┐
│ │ Monitoratge │
│ │ (Monitoring) │ Drift, errors, latència
│ └────────┬───────────────────┘
│ │
│ ▼
│ Reentrenar?
│ │
└── Sí ─────┘
El pipeline és el producte
La diferència entre un projecte de ML que funciona al portàtil i un que funciona en producció no és el model — és el pipeline. Sense pipeline, cada pas és manual i propens a errors: “he entrenat un model nou, però he oblidat validar-lo”, “quin preprocessador vaig usar?”. Amb pipeline:
- Reproducibilitat: Mateix input → mateix output, sempre
- Automatització: Un sol comando executa tot el procés
- Quality gates: Decisions automàtiques (si F1 < llindar → no desplegar)
- Traçabilitat: Cada execució queda registrada amb versions i mètriques
En MLOps, el pipeline és el producte. El model és una sortida del pipeline. Si canvien les dades o els requisits, no cal entrenar un model nou manualment — només cal re-executar el pipeline.
MLOps vs. DevOps: què canvia?
Si coneixeu CI/CD de desenvolupament de software convencional, el concepte de pipeline us sonarà familiar. Però els pipelines de ML tenen particularitats importants:
| DevOps (software) | MLOps (ML) | |
|---|---|---|
| Artefacte principal | Codi compilat → determinista | Model entrenat → depèn de codi + dades + config |
| Què valida la quality gate | Tests passen, codi compila | Tests passen + mètriques del model per sobre d’un llindar |
| Degradació | Bugs al codi → errors explícits (crashes) | Bugs + data drift → degradació silenciosa (el sistema funciona però les prediccions empitjoren) |
| Reentrenament | No existeix | Etapa fonamental — el model es degrada amb el temps |
| Versionat | Codi (git) | Codi (git) + dades + model + configuració |
En resum: un pipeline MLOps és un pipeline DevOps ampliat amb dades i experimentació. Les bones pràctiques de DevOps (automatització, testing, CI/CD) segueixen aplicant-se, però cal afegir-hi la gestió de dades, la validació de mètriques i el cicle de reentrenament.
Vocabulari clau
Quatre termes que apareixeran al llarg del curs:
- Etapes (stages): Cada pas fa una cosa específica — al diagrama en veiem sis: preprocessament, entrenament, validació, desplegament, monitoratge, i la decisió de reentrenar
- Artefactes (artifacts): Sortides de cada etapa — fitxers
.parquet,.pkl,metadata.json, logs de producció - Quality gates (portes de qualitat): Punts de decisió automàtics entre etapes — per exemple, la validació compara el model candidat amb l’anterior i atura el pipeline si no millora
- Triggers (activadors): Què inicia el pipeline — manual, programat, o en resposta a un event (drift detectat, degradació de latència)
En aquest capítol veurem les tecnologies bàsiques per construir les primeres etapes del pipeline: el data pipeline (preparació de dades en Parquet), el training pipeline (serialització de models), i el servei de predicció (FastAPI + Docker). Als capítols següents completarem el cicle: validació i CI/CD (Cap. 2), monitoratge de drift i errors (Cap. 3), i reentrenament continu (Cap. 4).
Comencem pel primer pilar: el desplegament. Un cop hem entrenat un model de machine learning que funciona bé en el nostre entorn de desenvolupament, ens enfrontem a un repte completament diferent: fer que aquest model estigui disponible per a usuaris reals, de manera fiable i escalable. Aquest procés s’anomena desplegament (deployment) i és on molts projectes de ML fallen. Segons diverses estimacions de la indústria, menys del 50% dels models entrenats arriben mai a producció.
En aquest capítol aprendrem els fonaments del desplegament de models, introduirem les tecnologies que ens permetran empaquetar i servir els nostres models (Docker i FastAPI), i explorarem les decisions arquitectòniques que haurem de prendre.
Per què el desplegament és difícil?
Quan entrenem un model, treballem en un entorn controlat: tenim les nostres llibreries instal·lades, les versions que ens agraden, i executem el codi manualment. En producció, tot canvia:
- El model ha de funcionar en una màquina diferent, potser amb un sistema operatiu diferent
- Ha de respondre a peticions de múltiples usuaris simultàniament
- Ha de ser actualitzable sense interrompre el servei
- Ha de gestionar errors de manera elegant
- Ha de ser monitoritzable per saber si funciona correctament
La solució a molts d’aquests problemes passa per contenidors i APIs.
Preparació de dades per producció
Abans de desplegar un model, les dades han de passar per un pipeline que les prepari per a l’entorn de producció. Prèviament, durant el curs, heu après a dividir dades en splits (60/20/20) i preprocessar-les. Aquí ens centrem en dos aspectes específics del desplegament: el format de dades i la connexió conceptual entre test set i producció.
Per què Parquet i no CSV?
Quan treballem en desenvolupament, sovint usem CSV perquè és llegible i fàcil de compartir. Però per a producció, Parquet és el format recomanat.
Problemes del CSV:
- Pèrdua de tipus: Tot es guarda com a text. Un enter
42i un string"42"són idèntics en CSV. Quan el carregues, pandas ha d’endevinar els tipus. - Ineficient: Sense compressió, ocupa molt espai.
- Lent: Cal llegir tot el fitxer per accedir a qualsevol dada.
Avantatges de Parquet:
- Preserva tipus: Enters, floats, strings, dates… es guarden amb el seu tipus original. No hi ha ambigüitat.
- Comprimit: Típicament 2-10x més petit que CSV.
- Ràpid: Format columnar, permet llegir només les columnes necessàries.
- Estàndard de la indústria: Compatible amb Spark, BigQuery, Snowflake, etc.
Conversió de CSV a Parquet:
import pandas as pd
# Llegir CSV
df = pd.read_csv('data/raw_data.csv')
# Guardar com Parquet
df.to_parquet('data/processed_data.parquet', index=False)
# Llegir Parquet (més ràpid, preserva tipus)
df = pd.read_parquet('data/processed_data.parquet')
Comparativa de mida i velocitat (exemple amb 100,000 registres):
| Format | Mida | Temps lectura |
|---|---|---|
| CSV | 15 MB | 1.2s |
| Parquet | 3 MB | 0.1s |
Per a projectes de ML en producció, sempre guardeu els splits processats en Parquet.
El test set com a simulació de producció
Quan dividim les dades en 60/20/20, el tercer split té un nom que pot confondre: test set. Però des de la perspectiva de desplegament, és més precís pensar-hi com a production set — dades que simulen el que el model veurà en producció.
Dataset complet
│
├── Training (60%) → Entrenar el model
│
├── Validation (20%) → Ajustar hiperparàmetres
│
└── Production (20%) → Simula dades reals de producció
Per què aquesta perspectiva és important?
El test/production set té dues funcions crítiques per al desplegament:
-
Avaluació final: Abans de desplegar, avaluem el model en dades que mai ha vist. Això ens diu com funcionarà amb usuaris reals.
-
Baseline per detectar drift: Després del desplegament, necessitem una referència per saber si les dades de producció han canviat. El production set és aquesta referència.
Abans del desplegament:
model.evaluate(production_set) → accuracy 0.87 ✓ Llest per desplegar
Després del desplegament (setmanes més tard):
dades_reals = get_production_data()
compare_distribution(production_set, dades_reals) → drift detectat! ⚠️
Per això mai s’ha de “contaminar” el production set durant el desenvolupament. Si l’uses per ajustar hiperparàmetres o prendre decisions, deixa de ser una simulació fiable de producció.
Pipeline de dades per producció
Apliquem el concepte de pipeline a la preparació de dades. El pipeline de preprocessament segueix aquest flux:
CSV original → Validar → Dividir (60/20/20) → Preprocessar → Guardar Parquet
Exemple d’estructura de fitxers després del pipeline:
data/
├── heartdisease.csv # Dades originals (CSV)
├── training_set.parquet # 60% - per entrenar
├── validation_set.parquet # 20% - per validar
└── production_set.parquet # 20% - simula producció
⚠️ Recordatori: El preprocessador (scaler, encoder) s’entrena només amb training data i s’aplica als altres splits. Això ja ho heu vist al curs de ML, però és crític per a producció: el preprocessador s’ha de guardar i usar exactament igual quan arribin dades reals.
Amb les dades preparades en format Parquet i el production set reservat, estem llestos per entrenar el model i desplegar-lo.
Etapa d’Entrenament (Training pipeline). L’entrenament del model no es tracta en aquest capítol perquè ja s’ha treballat a fons durant el curs de Machine Learning. Des del punt de vista del pipeline, l’important és que l’entrenament rep les dades preprocessades i produeix dos artefactes: el model i els preprocessadors entrenats, que cal serialitzar per poder-los desplegar.
Serialització de models
Quan entrenem un model de machine learning, aquest existeix només a la memòria RAM del nostre programa. Si tanquem Python, el model desapareix. Per poder desplegar un model, necessitem serialitzar-lo: convertir l’objecte Python a un format que es pugui guardar a disc i recuperar més tard.
Què és la serialització?
La serialització (serialization) és el procés de convertir un objecte en memòria a una seqüència de bytes que es pot:
- Guardar a disc com a fitxer
- Transmetre per xarxa
- Carregar més tard en un altre procés o màquina
El procés invers s’anomena deserialització: llegir els bytes i reconstruir l’objecte original en memòria.
┌─────────────────┐ serialitzar ┌─────────────────┐
│ Model entrenat │ ─────────────────► │ Fitxer a disc │
│ (memòria RAM) │ │ (.pkl, .pt, │
│ │ ◄───────────────── │ .json...) │
└─────────────────┘ deserialitzar └─────────────────┘
Per què és essencial per al desplegament?
Sense serialització, hauríem de reentrenar el model cada cop que volem fer prediccions. Això és:
- Lent: L’entrenament pot trigar hores o dies
- Costós: Requereix dades i recursos computacionals
- Impràctic: En producció necessitem respostes en mil·lisegons
Amb serialització, el flux és:
- Entrenar el model una vegada (pot trigar hores)
- Serialitzar el model a un fitxer
- Desplegar el fitxer al servidor de producció
- Deserialitzar el model a l’inici de l’aplicació
- Predir instantàniament (el model ja està entrenat)
Eines de serialització per llibreria
Cada llibreria de ML té les seves eines de serialització recomanades. Vegem les més comunes:
Scikit-learn: joblib i pickle
Per a models de scikit-learn, joblib és l’opció recomanada. Tot i estar construït sobre pickle, afegeix optimitzacions per a arrays de NumPy (compressió, memory-mapping) que el fan més eficient per a models sklearn.
⚠️ Seguretat: Com que joblib usa pickle internament, mai carreguis fitxers .pkl de fonts no fiables. Pickle pot executar codi arbitrari durant la deserialització.
import joblib
from sklearn.ensemble import RandomForestClassifier
# Entrenar el model
model = RandomForestClassifier(n_estimators=100)
model.fit(X_train, y_train)
# Serialitzar (guardar)
joblib.dump(model, 'models/random_forest.pkl')
# Deserialitzar (carregar)
model = joblib.load('models/random_forest.pkl')
prediction = model.predict(X_new)
pickle (de la llibreria estàndard de Python) també funciona, però és menys eficient per objectes grans:
import pickle
# Guardar
with open('models/model.pkl', 'wb') as f:
pickle.dump(model, f)
# Carregar
with open('models/model.pkl', 'rb') as f:
model = pickle.load(f)
Recomanació: Usa joblib per a sklearn. És més ràpid i genera fitxers més petits.
PyTorch: torch.save i TorchScript
PyTorch ofereix diverses opcions de serialització amb diferents avantatges. Les presentem de més simple a més avançada:
Opció 1: Guardar el model complet (més simple)
import torch
import torch.nn as nn
# Definir i entrenar el model
class NeuralNetwork(nn.Module):
def __init__(self):
super().__init__()
self.layers = nn.Sequential(
nn.Linear(10, 64),
nn.ReLU(),
nn.Linear(64, 1)
)
def forward(self, x):
return self.layers(x)
model = NeuralNetwork()
# ... entrenar ...
# Guardar model complet (arquitectura + pesos)
torch.save(model, 'models/model_complete.pt')
# Carregar (no cal definir la classe)
model = torch.load('models/model_complete.pt')
model.eval() # Mode avaluació (desactiva dropout, etc.)
Per a principiants: Aquesta opció és la més similar a
joblibde sklearn. Guarda tot el model en un sol fitxer i el carrega directament. És ideal per començar.
Opció 2: Guardar només els pesos amb state_dict (recomanat per PyTorch)
Aquesta és la pràctica recomanada oficialment per PyTorch perquè separa l’arquitectura (codi) dels pesos apresos (dades):
# Guardar només els pesos (state_dict)
torch.save(model.state_dict(), 'models/model_weights.pt')
# Carregar: cal recrear l'arquitectura primer!
model = NeuralNetwork() # Crear instància buida
model.load_state_dict(torch.load('models/model_weights.pt'))
model.eval()
Opció 3: TorchScript (recomanat per producció)
TorchScript compila el model a un format optimitzat i independent de Python:
# Convertir a TorchScript
scripted_model = torch.jit.script(model)
# o amb tracing (per models sense control flow dinàmic):
# scripted_model = torch.jit.trace(model, example_input)
# Guardar
scripted_model.save('models/model_scripted.pt')
# Carregar (pot executar-se sense Python!)
loaded_model = torch.jit.load('models/model_scripted.pt')
Opció 4: SafeTensors (format modern i segur)
SafeTensors és un format modern dissenyat per ser segur i eficient. A diferència de pickle, no permet execució de codi arbitrari durant la càrrega:
from safetensors.torch import save_file, load_file
# Guardar
save_file(model.state_dict(), 'models/model.safetensors')
# Carregar
state_dict = load_file('models/model.safetensors')
model = NeuralNetwork()
model.load_state_dict(state_dict)
model.eval()
| Mètode | Avantatges | Desavantatges |
|---|---|---|
| Model complet | Simple, ideal per aprendre | Menys portable, risc de seguretat amb pickle |
state_dict | Flexible, pràctica recomanada per PyTorch | Cal tenir la classe definida |
| TorchScript | Optimitzat, portable, pot executar-se en C++ | Algunes operacions no suportades |
| SafeTensors | Segur, ràpid, usat per Hugging Face | Cal instal·lar safetensors |
XGBoost: format natiu
XGBoost té el seu propi format de serialització, que és més eficient i segur que pickle:
import xgboost as xgb
# Entrenar el model
model = xgb.XGBClassifier(n_estimators=100)
model.fit(X_train, y_train)
# Guardar en format UBJSON (per defecte des de XGBoost 2.1)
model.save_model('models/xgboost_model.ubj')
# O en format JSON (llegible per humans)
model.save_model('models/xgboost_model.json')
# Carregar
model = xgb.XGBClassifier()
model.load_model('models/xgboost_model.ubj')
Nota: El format .ubj (Universal Binary JSON) és el format per defecte des de XGBoost 2.1, més compacte i sense pèrdua de precisió en nombres decimals. El format .json és llegible per humans, útil per inspeccionar l’estructura del model.
Format universal: ONNX
ONNX (Open Neural Network Exchange) és un format obert que permet interoperabilitat entre diferents frameworks. Un model exportat a ONNX es pot executar amb qualsevol runtime compatible, independentment de si es va entrenar amb PyTorch, TensorFlow, o sklearn.
Avantatges d’ONNX:
- Portable entre llenguatges (Python, C++, Java, JavaScript…)
- Optimitzacions de runtime (ONNX Runtime és molt ràpid)
- Desplegament en dispositius edge (mòbils, IoT)
Resum de formats recomanats
| Llibreria | Format recomanat | Extensió | Notes |
|---|---|---|---|
| scikit-learn | joblib | .pkl | Inclou preprocessadors |
| PyTorch | torch.save (model complet) | .pt | SafeTensors (.safetensors) per seguretat |
| XGBoost | Natiu UBJSON | .ubj | .json per inspecció manual |
| Cross-platform | ONNX | .onnx | Per producció optimitzada |
Bones pràctiques de serialització
- Serialitza sempre els preprocessadors: Scalers, encoders, etc. han d’anar amb el model
- Documenta les versions: La compatibilitat pot trencar-se entre versions de llibreries
- Usa formats natius quan puguis: Són més segurs que pickle genèric
- Considera ONNX per producció: Especialment si necessites rendiment o portabilitat
- Testa la càrrega: Sempre verifica que el model carregat fa les mateixes prediccions
Etapa de Validació (Quality gate). Abans de desplegar un model serialitzat, cal verificar que compleix uns criteris mínims de qualitat: mètriques per sobre d’un llindar, comparació amb el model anterior, etc. Si no passa la validació, el pipeline s’atura — mai es desplega un model que no ha estat validat. Implementarem aquesta etapa al capítol 2.
Predicció online vs. predicció batch
Hi ha dues estratègies principals per servir prediccions, i la tria depèn del cas d’ús.
Predicció online (en temps real)
La predicció online (online prediction) és quan el model rep una petició i ha de respondre immediatament. És el que veurem amb FastAPI a la següent secció.
Característiques:
- Latència baixa (mil·lisegons)
- Una predicció per petició (o poques)
- El model està carregat en memòria constantment
- Requereix infraestructura sempre disponible
Casos d’ús:
- Recomanacions en temps real
- Detecció de frau en transaccions
- Assistents virtuals
- Cerca personalitzada
Predicció batch (per lots)
La predicció batch (batch prediction) és quan processem moltes prediccions alhora, normalment de manera programada.
Característiques:
- La latència no és crítica (minuts o hores acceptable)
- Milers o milions de prediccions alhora
- El model es carrega, processa, i s’atura
- Més eficient en recursos per a grans volums
- Les prediccions es guarden en una base de dades per ser consultades després
Casos d’ús:
- Enviament massiu de correus personalitzats
- Càlcul nocturn de puntuacions de risc
- Generació de recomanacions pre-calculades
- Informes periòdics
Flux típic d’una predicció batch:
- Dades acumulades: Es recullen i acumulen les dades que s’han de processar.
- Script batch: S’executa un script o procés per iniciar el lot de prediccions.
- Carregar model: El model de machine learning es carrega a la memòria.
- Processar tot: Es processen totes les dades d’una sola vegada, aplicant el model a cada cas.
- Guardar a base de dades: Els resultats es desen en una base de dades (o fitxers) per a consulta posterior.
La diferència clau amb predicció online és que els resultats no es retornen immediatament a l’usuari. En lloc d’això, es guarden i l’aplicació els consulta quan els necessita:
Comparativa
| Aspecte | Online | Batch |
|---|---|---|
| Latència | Mil·lisegons | Minuts/hores |
| Volum per execució | Baix | Alt |
| Recursos | Sempre actius | Sota demanda |
| Complexitat | Més alta | Més baixa |
| Freshness de prediccions | Temps real | Periòdic |
| On es guarden resultats | Retorn immediat | Base de dades |
Arquitectura híbrida
En molts sistemes reals es combinen ambdues estratègies:
- Batch per pre-calcular prediccions per als casos més comuns
- Online per als casos nous o que requereixen context en temps real
Per exemple, una botiga online podria pre-calcular recomanacions cada nit per a tots els usuaris (batch), però calcular en temps real les recomanacions basades en el que l’usuari està mirant ara mateix (online).
A continuació, veurem com implementar predicció online amb FastAPI.
Servir models amb FastAPI
Per què una API?
Per fer que el nostre model sigui accessible, necessitem exposar-lo a través d’una API (Application Programming Interface). Una API web permet que altres aplicacions facin peticions al nostre model i rebin prediccions com a resposta.
El protocol més comú per a APIs web és HTTP, i l’arquitectura més utilitzada és REST (Representational State Transfer). En una API REST, fem peticions a URLs específiques (anomenades endpoints) utilitzant mètodes HTTP com GET o POST.
FastAPI: modern i ràpid
FastAPI és un framework de Python per crear APIs. Els seus avantatges principals són:
- Ràpid: Un dels frameworks més ràpids de Python, comparable a Node.js o Go
- Fàcil: Sintaxi intuïtiva basada en tipus de Python
- Documentació automàtica: Genera documentació interactiva automàticament
- Validació automàtica: Valida les dades d’entrada automàticament
Exemple bàsic d’una API per servir un model
from fastapi import FastAPI
from pydantic import BaseModel
import joblib
import numpy as np
model = joblib.load("model/model.pkl")
app = FastAPI(title="API de Predicció")
class InputData(BaseModel):
feature_1: float
feature_2: float
feature_3: float
@app.post("/predict")
def predict(data: InputData):
features = np.array([[data.feature_1, data.feature_2, data.feature_3]])
result = model.predict(features)[0]
return {"prediction": float(result), "model_version": "1.0.0"}
@app.get("/health")
def health():
return {"status": "healthy"}
L’estructura bàsica és: carregar model a l’inici, definir schema d’entrada amb Pydantic, exposar endpoint de predicció i health check.
Integrant FastAPI amb Docker
El Dockerfile complet per a aquesta aplicació seria:
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY model/ ./model/
COPY main.py .
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
On requirements.txt contindria:
fastapi>=0.109.0,<1.0.0
uvicorn[standard]>=0.27.0,<1.0.0
joblib>=1.4.0,<2.0.0
scikit-learn>=1.4.0,<2.0.0
pydantic>=2.5.0,<3.0.0
numpy>=1.24.0,<2.0.0
Versionat de models
Un aspecte crític del desplegament de models és el versionat: mantenir un registre clar de quins models hem desplegat, quan, i amb quines característiques. Sense versionat, és impossible fer rollback, comparar models, o simplement saber què està en producció.
Per què versionar models?
Problemes sense versionat:
- “Quin model està en producció ara?” → No ho sabem
- “Aquest model funciona malament, tornem a l’anterior” → Quin era?
- “Aquest bug existia en la v1.2?” → No tenim v1.2, només
model.pkl
Avantatges del versionat:
- Traçabilitat: Saber exactament què està desplegat
- Rollback: Tornar a versions anteriors si cal
- Comparació: Avaluar si un nou model és millor
- Reproducibilitat: Reconstruir models antics si cal
- Auditoria: Complir amb requisits regulatoris
Semantic Versioning per models
Adaptem semantic versioning (MAJOR.MINOR.PATCH) per a models ML:
v1.2.3
│ │ │
│ │ └─ PATCH: Bug fixes en preprocessament, mateix model i features
│ └─── MINOR: Mateix algoritme, afegir features o reentrenament amb més dades
└───── MAJOR: Nou algoritme o arquitectura significativament diferent
Exemples:
v1.0.0 → v1.0.1: Fix en normalització, mateix modelv1.0.1 → v1.1.0: Afegida feature “region”, mateix RandomForestv1.1.0 → v2.0.0: Canvi de RandomForest a XGBoost
Guardar models amb versions
Cada model s’ha de guardar amb un fitxer de metadata que inclogui la versió, mètriques i features:
models/
├── model_v1.2.0.pkl
└── metadata_v1.2.0.json
Contingut de metadata_v1.2.0.json:
{
"version": "1.2.0",
"trained_at": "2024-01-15T14:30:00",
"metrics": {
"f1": 0.82,
"recall": 0.85,
"precision": 0.79
},
"features": ["age", "income", "score", "region"],
"model_type": "RandomForestClassifier"
}
Symlink per “model actual”
Per facilitar el desplegament, crear un symlink que apunti al model actual:
# En Linux/Mac
ln -sf model_v1.2.0.pkl current_model.pkl
# Ara podem carregar sempre current_model.pkl
model = joblib.load('models/current_model.pkl')
Quan despleguem una nova versió:
# Actualitzar symlink a nova versió
ln -sf model_v1.3.0.pkl current_model.pkl
# Rollback si cal
ln -sf model_v1.2.0.pkl current_model.pkl
Per a entorns més complexos, la metadata pot incloure també informació del dataset (hash, nombre de registres), hiperparàmetres, versions de llibreries, i informació de desplegament.
Model registry simple
Un model registry és un lloc centralitzat per guardar i gestionar models. Pot ser tan simple com un directori amb convencions:
models/
├── v1.0.0/
│ ├── model.pkl
│ ├── metadata.json
│ └── scaler.pkl
├── v1.1.0/
│ ├── model.pkl
│ ├── metadata.json
│ └── scaler.pkl
├── v1.2.0/
│ ├── model.pkl
│ ├── metadata.json
│ └── scaler.pkl
└── current -> v1.2.0/ # Symlink
Incloure versió a l’API
Incloure la versió del model a les respostes de l’API permet traçar quina versió va fer cada predicció:
{
"prediction": 0.85,
"model_version": "1.2.0"
}
Resum del versionat
Essencial:
- ✅ Versió clara per cada model (
v1.2.0) - ✅ Metadata amb mètriques i features
- ✅ Convencions de noms (
model_v1.2.0.pkl,metadata_v1.2.0.json)
Recomanat:
- ✅ Symlink
current_model.pklper facilitar desplegament - ✅ Directori per versió amb model + metadata + preprocessadors
- ✅ Versió inclosa a les respostes de l’API
Avançat:
- ✅ Metadata extesa (dataset hash, hyperparameters, entorn)
- ✅ Model registry centralitzat (MLflow, DVC, o directori estructurat)
- ✅ CI/CD que gestiona versions automàticament
Amb versionat adequat, tenim control complet sobre els models desplegats!
Ara que tenim el model versionat i servit via API, necessitem garantir que funciona igual en qualsevol entorn: el nostre ordinador, el servidor de staging (un entorn de proves que replica producció però no és públic), i producció.
Contenidors amb Docker
Què és un contenidor?
Un contenidor (container) és una unitat de programari que empaqueta el codi i totes les seves dependències perquè l’aplicació s’executi de manera ràpida i fiable en diferents entorns. Podem pensar en un contenidor com una “capsa” que conté tot el necessari per executar la nostra aplicació: el codi, les llibreries, les configuracions, i fins i tot una versió mínima del sistema operatiu.
┌─────────────────────────────────────┐
│ Contenidor │
│ ┌───────────────────────────────┐ │
│ │ La nostra aplicació (model) │ │
│ ├───────────────────────────────┤ │
│ │ Python 3.11, scikit-learn, │ │
│ │ FastAPI, pandas... │ │
│ ├───────────────────────────────┤ │
│ │ Sistema operatiu mínim │ │
│ │ (Ubuntu, Alpine...) │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘
Docker: l’eina estàndard
Docker és l’eina més utilitzada per crear i gestionar contenidors. Els conceptes clau són:
- Imatge (image): Una plantilla de només lectura amb les instruccions per crear un contenidor. És com un “motlle”.
- Contenidor (container): Una instància executable d’una imatge. És el “pastís” fet a partir del motlle.
- Dockerfile: Un fitxer de text amb les instruccions per construir una imatge.
- Docker Compose: Una eina per definir i executar aplicacions amb múltiples contenidors.
Estructura bàsica d’un Dockerfile
Un Dockerfile per a un model de ML típicament té aquesta estructura:
# Imatge base amb Python
FROM python:3.11-slim
# Directori de treball dins del contenidor
WORKDIR /app
# Copiar i instal·lar dependències
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copiar el codi i el model
COPY model/ ./model/
COPY app.py .
# Port on escoltarà l'aplicació
EXPOSE 8000
# Comanda per executar l'aplicació
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
Per què requirements.txt?
El fitxer requirements.txt és fonamental per al desplegament de models. Sense ell, les dependències del projecte quedarien indefinides i el model podria fallar en producció per incompatibilitats de versions.
Avantatges clau:
-
Reproducibilitat: Garanteix que tothom (i cada servidor) instal·la exactament les mateixes versions de les llibreries. Sense versions fixades,
pip install scikit-learnpodria instal·lar versions diferents avui i demà. -
Aïllament d’errors: Si una nova versió d’una llibreria trenca el codi, pots identificar-ho ràpidament comparant versions.
-
Eficiència amb Docker: Docker guarda cada instrucció del Dockerfile en una capa (layer) que es reutilitza si no ha canviat. Copiar
requirements.txtabans del codi permet que Docker reutilitzi la capa de dependències mentre només el codi canvia.
Com crear i usar requirements.txt:
# Guardar les dependències actuals del projecte
pip freeze > requirements.txt
# Instal·lar totes les dependències des del fitxer
pip install -r requirements.txt
pip freeze genera una llista de totes les llibreries instal·lades amb les seves versions exactes. Això és útil per capturar l’estat actual de l’entorn, però pot incloure dependències transitives que no necessites explícitament.
Bones pràctiques:
# Especificar rangs de versions (recomanat)
fastapi>=0.109.0,<1.0.0
scikit-learn>=1.4.0,<2.0.0
# O versions exactes (màxima reproducibilitat)
fastapi==0.109.2
scikit-learn==1.4.0
Usar rangs (>=1.4.0,<2.0.0) permet actualitzacions de seguretat dins d’una versió major, mentre que versions exactes (==1.4.0) garanteixen reproducibilitat total però requereixen més manteniment.
Com aplicar actualitzacions de seguretat:
Els rangs de versions no s’actualitzen automàticament. Per obtenir les versions més noves dins del rang permès:
# Actualitzar totes les dependències al màxim permès pels rangs
pip install --upgrade -r requirements.txt
Amb pip-tools, pots regenerar el fitxer amb les versions més recents:
# Regenerar requirements.txt amb les últimes versions permeses
pip-compile --upgrade requirements.in
pip install -r requirements.txt
Sense --upgrade, pip-compile manté les versions existents si encara compleixen els rangs. Amb --upgrade, ignora el requirements.txt actual i resol totes les dependències de nou per obtenir les últimes versions permeses.
Consells segons el context:
-
Per a projectes simples o en desenvolupament: Crea el fitxer manualment amb només les dependències directes del projecte. Pip resoldrà automàticament les dependències transitives. Això fa el fitxer més net i fàcil de mantenir.
-
Per a producció o màxima reproducibilitat: Usa
pip-toolsper combinar mantenibilitat amb control total. Crea un fitxerrequirements.inamb les dependències directes, i generarequirements.txtambpip-compile:
# requirements.in (mantingut manualment)
fastapi>=0.109.0,<1.0.0
scikit-learn>=1.4.0,<2.0.0
# Generar requirements.txt amb totes les versions fixades
pip-compile requirements.in
Això genera un requirements.txt amb totes les dependències (directes i transitives) amb versions exactes, garantint que l’entorn sigui idèntic en cada instal·lació.
El flux de treball amb Docker
El procés típic és:
- Escrivim el Dockerfile que descriu com construir la imatge
- Construïm la imatge localment amb
docker build - Provem el contenidor localment amb
docker run - Pugem la imatge a un registre (pot ser un registre privat al nostre servidor)
- Al servidor de producció, descarreguem i executem la imatge
Docker Compose: orquestració simplificada
Fins ara hem vist com crear una imatge Docker, però quan despleguem aplicacions reals, sovint necessitem múltiples contenidors treballant junts (per exemple, API + base de dades) o simplement volem configurar més fàcilment un sol contenidor. Aquí és on entra Docker Compose.
Docker Compose és una eina per definir i executar aplicacions Docker amb múltiples contenidors usant un fitxer YAML.
Quan usar Docker Compose?
- Configuració complexa: Moltes variables d’entorn, volums, ports
- Múltiples serveis: API + base de dades + cache
- Desenvolupament local: Levantar tot l’entorn amb un sol comando
- Reproduibilitat: Configuració compartible i versionable
Estructura bàsica d’un compose.yml
# compose.yml
services:
api:
build: .
ports:
- "8000:8000"
environment:
- MODEL_PATH=/models/model.pkl
- LOG_LEVEL=info
volumes:
- ./models:/models
- ./logs:/logs
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
Explicació dels camps:
- services: Defineix els contenidors (aquí només n’hi ha un:
api) - build: Directori amb el Dockerfile
- ports: Mapatge port host:contenidor (
8000:8000= port 8000 del host → port 8000 del contenidor) - environment: Variables d’entorn
- volumes: Muntar directoris del host al contenidor
- healthcheck: Comanda per verificar que el servei està sa
Health checks: el contracte de desplegament
Els health checks són un estàndard universal en sistemes de producció. No són només una bona pràctica, sinó un contracte de desplegament que permet a la infraestructura saber si el servei està operatiu.
Per què són fonamentals?
- Docker / Docker Swarm: Marca contenidors com “unhealthy” i pot reiniciar-los automàticament
- Kubernetes: Usa readiness probes (servei llest?) i liveness probes (servei encara viu?)
- Load balancers (AWS ELB, NGINX): Només envia tràfic a instàncies que responen health checks
- CI/CD pipelines: Verifica que el desplegament ha tingut èxit
- Monitoring systems (Prometheus, Datadog): Alerten quan el servei no respon
Configuració a Docker Compose:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s # Cada quan executar el check
timeout: 10s # Temps màxim per resposta
retries: 3 # Quants errors abans de marcar com unhealthy
start_period: 40s # Temps d'espera inicial (per permetre arrencada)
Aquest health check crida l’endpoint /health cada 30 segons. Si falla 3 vegades seguides, Docker marca el contenidor com “unhealthy”.
Endpoint de health a FastAPI:
@app.get("/health")
def health():
"""Health check endpoint."""
return {
"status": "healthy",
"model_loaded": model is not None,
"timestamp": datetime.now().isoformat()
}
Noms d’endpoint comuns:
Diferents organitzacions usen diferents convencions:
/health- El més comú/healthz- Estil Kubernetes/status- Alternatiu/api/health- Si l’API està sota/api
L’important és que:
- Retorni HTTP 200 quan el servei està operatiu
- Sigui ràpid (< 100ms idealment)
- No requereixi autenticació (ha de ser accessible per l’orquestrador)
- Verifiqui dependències crítiques (model carregat, base de dades accessible si és crítica)
Readiness vs Liveness (concepte Kubernetes):
- Liveness: “Estic viu?” - Si falla, reinicia el contenidor
- Readiness: “Estic llest per rebre tràfic?” - Si falla, para d’enviar tràfic però no reinicia
En sistemes simples, un sol endpoint /health fa ambdues funcions. En sistemes complexos, poden ser endpoints diferents (/health/live i /health/ready).
Comandes de Docker Compose
# Construir i iniciar tots els serveis
docker compose up --build
# Iniciar en mode detached (background)
docker compose up -d
# Veure logs
docker compose logs -f
# Aturar serveis
docker compose down
# Veure estat dels serveis
docker compose ps
Exemple complet: API amb configuració
# compose.yml
services:
model-api:
build:
context: .
dockerfile: Dockerfile
container_name: ml_model_api
ports:
- "8000:8000"
environment:
- MODEL_PATH=/app/models/model.pkl
- MODEL_VERSION=1.0.0
- LOG_LEVEL=info
- MAX_WORKERS=4
volumes:
- ./models:/app/models:ro # :ro = read-only
- ./logs:/app/logs
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
restart: unless-stopped # Reiniciar automàticament si falla
Avantatges d’aquesta configuració:
- Models com volum: Podem actualitzar el model sense reconstruir la imatge
- Logs persistents: Els logs es guarden al host
- Health check: Docker sap si el servei funciona
- Restart policy: El contenidor es reinicia automàticament si falla
Variables d’entorn: separar configuració i codi
En lloc de hardcoded values al Dockerfile, usem variables d’entorn:
# api.py
import os
MODEL_PATH = os.getenv('MODEL_PATH', 'models/model.pkl')
MODEL_VERSION = os.getenv('MODEL_VERSION', 'unknown')
LOG_LEVEL = os.getenv('LOG_LEVEL', 'info')
model = joblib.load(MODEL_PATH)
Això permet canviar configuració sense reconstruir:
environment:
- MODEL_PATH=/app/models/model_v2.pkl # Canviar model
- LOG_LEVEL=debug # Canviar log level
Bones pràctiques amb Docker
- Noms de serveis clars:
model-api,postgres, noservice1,app - Health checks sempre: Per detectar problemes ràpidament
- Volums per dades persistents: Models, logs, bases de dades
- Variables d’entorn: Mai hardcoded secrets o configuracions
- restart: unless-stopped: Per serveis que han d’estar sempre actius
- Fitxer .env: Per secrets (no comitear a git!)
- Utilitzar imatges minimalistes: Menys capa → arrencada més ràpida → menys vulnerabilitats
- Precarregar el model en arrencar: Evita la latència de carregar-lo quan arriba la primera petició
- Evitar dependències innecessàries: Menys mida, menys riscos, menys temps de build
- Monitoritzar sempre: Prometheus + Grafana per controlar latència, throughput, càrrega CPU/GPU i memòria
Amb Docker Compose, el desplegament és tan simple com docker compose up!
Múltiples configuracions per a diferents entorns
En projectes reals, sovint necessitem configuracions diferents per a desenvolupament, testing i producció. En lloc de tenir un sol compose.yml amb moltes variables d’entorn, podem usar múltiples fitxers de composició.
Per què múltiples fitxers?
Diferents entorns tenen diferents necessitats:
- Desenvolupament: Hot reload, logs verbosos, eines de debug
- Testing/Validació: Configuració mínima, execució de tests, fast startup
- Producció: Optimitzat per rendiment, logs estructurats, restart policies
Estratègia de tres fitxers
project/
├── compose.yml # Desenvolupament (hot reload, debug)
├── compose.validate.yml # Tests (pytest)
└── compose.deploy.yml # Producció (workers, healthcheck, restart)
Cada fitxer configura el mateix servei amb opcions diferents per a cada context. S’executen amb:
docker compose up # Desenvolupament
docker compose -f compose.validate.yml up # Tests
docker compose -f compose.deploy.yml up -d # Producció
Quan usar aquest patró?
Usar múltiples fitxers quan:
- Els entorns tenen necessitats molt diferents
- Vols configuracions clares i explícites
- Treballes en equip i cal separar responsabilitats
- Tens un pipeline de CI/CD amb múltiples etapes
Usar un sol fitxer quan:
- El projecte és simple
- Les diferències es poden gestionar amb variables d’entorn
- Només tens un o dos entorns
Aquest patró és especialment útil en pipelines de CI/CD, on cada etapa (validació, staging, producció) necessita una configuració diferent.
CI/CD: Integració i desplegament continus
Per garantir que tot el codi passi validació abans d’arribar a producció, necessitem automatització al servidor. Aquí és on entra CI/CD.
Què és CI/CD?
CI/CD (Continuous Integration / Continuous Deployment) és una pràctica estàndard en el desenvolupament de software modern que automatitza la validació i el desplegament del codi.
Continuous Integration (CI) - Integració contínua:
- Cada cop que un desenvolupador fa push al repositori, s’executen automàticament tests i validacions
- L’objectiu és detectar errors d’integració aviat, quan són fàcils de corregir
- Sense CI, els problemes es descobreixen tard (quan s’intenta fer merge o desplegar), quan ja són costosos de resoldre
Continuous Deployment (CD) - Desplegament continu:
- Un cop el codi passa totes les validacions, es desplega automàticament a producció (o a un entorn de staging)
- Elimina el procés manual de desplegament, que és propens a errors
- Permet releases freqüents i incrementals
Per què CI/CD és important?
Sense CI/CD, el flux de treball típic és:
- Desenvolupador escriu codi durant dies/setmanes
- Intenta fer merge → Conflictes amb codi d’altres
- Es descobreixen bugs que interaccionen amb altres canvis
- Desplegament manual → Errors humans, passos oblidats
Amb CI/CD:
- Desenvolupador fa push de canvis petits freqüentment
- Cada push es valida automàticament en minuts
- Errors detectats immediatament, fàcils de corregir
- Desplegament automatitzat, consistent i reproduïble
| Sense CI/CD | Amb CI/CD |
|---|---|
| Integració dolorosa (“merge hell”) | Integració contínua sense conflictes |
| Bugs descoberts tard | Bugs detectats en minuts |
| Desplegament manual, arriscat | Desplegament automàtic, segur |
| Releases grans i poc freqüents | Releases petites i freqüents |
Eines de CI/CD
Les plataformes més comunes són:
- GitHub Actions: Integrat amb GitHub, configuració amb fitxers YAML
- GitLab CI/CD: Integrat amb GitLab, molt potent
- Jenkins: Open-source, autohospedable, molt flexible
- CircleCI, Travis CI: Alternatives cloud populars
Totes funcionen amb el mateix principi: detectar push → executar tasques definides → bloquejar si fallen.
Alternatives lleugeres: git hooks i scripts
No sempre necessitem plataformes CI/CD completes. Per a projectes més petits o entorns sense accés a serveis externs, podem aconseguir el mateix objectiu amb:
- Git hooks al servidor: Qualsevol servidor git permet configurar hooks (com
post-receive) que executen tests i validen el model quan algú fa push. Si fallen, el push es rebutja. - Scripts personalitzats: Scripts de validació s’integren directament en qualsevol d’aquestes solucions.
- Versionat de models amb git: Guardar models amb noms versionats (
model_v1.0.0.pkl), mantenir fitxers JSON amb mètriques, i usar tags de git per marcar versions desplegades. Tot queda traçable sense dependències externes.
Recomanació: Comença amb scripts simples i git hooks. Afegeix complexitat només quan sigui necessari.
Al proper capítol veurem com adaptar CI/CD per a projectes ML: quins passos afegeix ML al pipeline, com integrar criteris de qualitat com a portes automàtiques, i com implementar git hooks per validació local i al servidor.
Resum
En aquest capítol hem après els fonaments del desplegament de models:
- Preparar dades amb splits adequats (60/20/20) i preprocessament consistent
- Triar entre predicció online (temps real) i batch (lots programats)
- Servir prediccions amb FastAPI, incloent versionat de models
- Empaquetar models amb Docker per garantir reproducibilitat entre entorns
- Automatitzar el desplegament amb CI/CD per validar i desplegar de manera consistent
Ara que sabem com desplegar models, al proper capítol aprendrem com garantir la seva qualitat i validació abans i després del desplegament.
Validació i qualitat
- Introducció
- Validació de models
- Testing de codi
- Type hints: validació estàtica
- Automatització de la validació
- Optimització en producció
- Resum
Introducció
Al capítol anterior vam aprendre com preparar i desplegar models de machine learning utilitzant Docker, FastAPI, i estratègies de predicció online i batch. Ara que sabem com desplegar, és moment d’aprendre com fer-ho bé.
Desplegar un model sense validació adequada és com publicar un llibre sense revisar-lo: pot funcionar, però els errors poden ser costosos. En aquest capítol explorarem:
- Criteris objectius per decidir si un model està llest per producció
- Testing específic per a sistemes de machine learning
- Automatització de la validació amb git hooks i pipelines CI/CD per a ML
- Optimització de models per a producció
Aquestes pràctiques garanteixen que només models validats i de qualitat arribin als teus usuaris.
Validació de models
Abans de posar un model en producció, necessitem establir criteris clars que ens diguin si el model és “prou bo”. Sense aquests criteris, podem acabar desplegant models que no funcionen adequadament o, al contrari, rebutjant models perfectament vàlids.
El problema de la validació subjectiva
Sense criteris clars:
- “Aquest F1 de 0.78 és bo?” → Depèn del context
- “Podem desplegar?” → No ho sabem
- “El nou model és millor?” → Difícil de comparar
Necessitem llindars objectius definits abans d’entrenar.
Tipus de criteris de desplegament
1. Mètriques tècniques
Rendiment mesurable del model en el validation set:
| Mètrica | Descripció | Exemple de llindar |
|---|---|---|
| Accuracy | Percentatge de prediccions correctes | > 0.85 |
| F1 Score | Mitjana harmònica de precision i recall | > 0.75 |
| Precision | % de positius predits que són correctes | > 0.80 |
| Recall | % de positius reals que detectem | > 0.70 |
| ROC-AUC | Capacitat de discriminar entre classes | > 0.85 |
| MAE/RMSE | Error en regressió | < 5000 (depèn de l’escala) |
2. Mètriques de negoci
Impacte real en el negoci o usuaris:
# Exemple: cost de classificació mèdica
cost_false_negative = 10_000 # € - malaltia no detectada
cost_false_positive = 500 # € - test innecessari
# Criteri: cost total < baseline
def business_cost(y_true, y_pred):
fn = ((y_true == 1) & (y_pred == 0)).sum()
fp = ((y_true == 0) & (y_pred == 1)).sum()
return fn * cost_false_negative + fp * cost_false_positive
Criteris de negoci típics:
- Cost de falsos positius vs falsos negatius
- Impacte en conversió / vendes
- Experiència d’usuari (latència, qualitat)
- ROI (retorn d’inversió)
3. Mètriques operacionals
Requisits d’infraestructura i rendiment:
| Mètrica | Descripció | Exemple de llindar |
|---|---|---|
| Latència p50 | Temps de resposta mitjà | < 50ms |
| Latència p95 | Temps de resposta del 95% de peticions | < 100ms |
| Latència p99 | Temps de resposta del 99% de peticions | < 200ms |
| Mida del model | Espai en disc / memòria | < 500 MB |
| Throughput | Prediccions per segon | > 100 req/s |
Definir llindars: el procés
Pas 1: Establir la baseline
Abans de desplegar el primer model, definir què és “acceptable”. Opcions per la baseline:
- Predicció constant: Sempre predir la classe més freqüent
- Regla heurística: Lògica simple basada en dominis (if-then)
- Model anterior: Si n’hi ha un en producció
- Requisit de negoci: Cost màxim acceptable
Exemple:
# Baseline: predicció constant (classe majoritària)
from sklearn.dummy import DummyClassifier
baseline = DummyClassifier(strategy='most_frequent')
baseline.fit(X_train, y_train)
baseline_f1 = f1_score(y_val, baseline.predict(X_val))
# baseline_f1 = 0.45
# Criteris: millor que baseline + marge
MIN_F1 = baseline_f1 + 0.10 # = 0.55
MIN_RECALL = 0.70 # Requisit de negoci
Pas 2: Consensuar amb stakeholders
Els criteris no són només tècnics. Cal involucrar:
- Product owners (impacte de negoci)
- Usuaris finals (experiència)
- Operacions (infraestructura)
- Legal/ètica (biaixos, fairness)
Pas 3: Documentar els criteris
# config/deployment_criteria.yaml
# Mètriques tècniques
min_accuracy: 0.80
min_f1: 0.75
min_recall: 0.70
# Mètriques operacionals (opcional)
max_latency_ms: 100
max_model_size_mb: 500
Implementar la validació
La validació automàtica consisteix en:
- Carregar els criteris des del fitxer YAML
- Calcular les mètriques del model candidat (F1, recall, precision)
- Comparar cada mètrica contra el llindar mínim
- Si ja hi ha un model en producció, verificar que el nou no és pitjor (tolerant una petita degradació, p.ex. 2%)
- Aprovar o rebutjar automàticament
L’script de validació (validate_model.py) pot retornar exit code 0 (aprovat) o 1 (rebutjat), integrant-se directament amb CI/CD i git hooks.
📖 Implementació pràctica: El patró
deployment_readyal capítol de patrons d’implementació explica com guardar el resultat d’aquesta validació en un flag dins del metadata del model, i com el CI/CD l’utilitza per bloquejar desplegaments de models no validats.
Resum: checklist de desplegament
Abans de desplegar un model, verifica:
- ✅ Criteris tècnics: F1, recall, precision compleixen llindars
- ✅ Criteris de negoci: Cost, ROI, impacte acceptable
- ✅ Criteris operacionals: Latència, mida, throughput acceptables
- ✅ Comparació: Nou model ≥ model actual (si n’hi ha)
- ✅ Baseline: Millor que predicció naive
- ✅ Documentat: Criteris escrits i versionats
- ✅ Consensuat: Stakeholders d’acord amb els criteris
Amb criteris clars, podem desplegar models amb confiança i justificar les decisions tècniques!
Testing de codi
Un cop hem desenvolupat el nostre servei de prediccions, necessitem assegurar-nos que funciona correctament abans de desplegar-lo. Però testejar sistemes de machine learning és diferent de testejar software tradicional.
Per què el testing ML és diferent?
En software tradicional, testem que el codi fa exactament el que esperem: suma(2, 3) ha de retornar 5, sempre. Però en ML, no podem testejar prediccions concretes perquè:
- Els models són estocàstics: L’entrenament té randomització, el model pot canviar
- L’accuracy fluctua: Un model millor podria donar diferents prediccions
- El que importa és el rendiment global, no prediccions individuals
Principi clau: Testem la infraestructura (el codi al voltant del model), no el model en si.
Testing vs. Validació: una distinció fonamental
En sistemes de machine learning, cal distingir clarament entre dos conceptes que sovint es confonen:
Testing: Components deterministes
Què és: Verificar que el codi funciona correctament amb comportament predictible i reproduïble.
Què testem:
- Funcions de validació de dades
- Preprocessament de features
- Càrrega de models
- Endpoints de l’API
- Lògica de negoci
- Gestió d’errors
Eina: pytest (tests automatitzats, execució ràpida)
Exemple:
def test_normalize_value():
"""Test determinista: sempre ha de retornar el mateix resultat."""
assert normalize(50, 0, 100) == 0.5
assert normalize(0, 0, 100) == 0.0
Validació: Comportament del model
Què és: Avaluar el rendiment estadístic del model amb comportament estocàstic i variable.
Què validem:
- Accuracy, precision, recall, F1
- ROC-AUC, precisió mitjana
- Mètriques de negoci
- Distribució de prediccions
- Calibratge del model
Eines: Validation set durant entrenament, MLflow per tracking, anàlisi exploratòria
Exemple:
# Això NO és un test pytest, és validació durant l'entrenament
def evaluate_model(model, X_val, y_val):
"""Validació: mètriques poden variar entre execucions."""
y_pred = model.predict(X_val)
f1 = f1_score(y_val, y_pred)
print(f"F1 score: {f1:.3f}") # Pot ser 0.823, després 0.819...
return f1
Per què aquesta distinció és crítica?
Problema: Si barregem testing i validació, obtenim tests inestables (flaky tests):
# ❌ MAL - Test inestable!
def test_model_accuracy():
model = train_model(X_train, y_train)
accuracy = model.score(X_test, y_test)
assert accuracy > 0.95 # Pot fallar aleatòriament!
Aquest test pot fallar per raons legítimes:
- Randomització diferent a l’entrenament
- Canvis en hiperparàmetres que milloren el model però canvien predictions
- Variabilitat normal en models probabilístics
Solució correcta:
# ✅ BÉ - Testejar infraestructura
def test_model_loads():
"""Verificar que el model es pot carregar."""
model = load_model('models/model.pkl')
assert model is not None
assert hasattr(model, 'predict')
def test_model_predicts_valid_format():
"""Verificar que les prediccions tenen el format correcte."""
model = load_model('models/model.pkl')
X = pd.DataFrame({'age': [30], 'income': [50000]})
predictions = model.predict(X)
assert len(predictions) == 1
assert isinstance(predictions[0], (int, np.integer, float, np.floating))
Validació durant entrenament (NO en pytest):
# Executar durant train.py, no com a test
def main():
model = train_model(X_train, y_train)
metrics = evaluate_model(model, X_val, y_val)
# Criteris de desplegament
if metrics['f1'] >= 0.75 and metrics['recall'] >= 0.70:
save_model(model, 'models/model_v2.pkl')
print("✓ Model validat i guardat")
else:
print("✗ Model no compleix criteris")
Taula resum
| Aspecte | Testing | Validació |
|---|---|---|
| Comportament | Determinista | Estocàstic |
| Què verifica | Codi funciona correctament | Model és prou bo |
| Eina principal | pytest | Validation set, MLflow |
| Execució | Cada commit (CI/CD) | Durant/després entrenament |
| Fallida | Bug en codi → cal arreglar | Rendiment insuficient → iterar model |
| Estabilitat | Ha de ser 100% estable | Pot variar lleugerament |
Regla d’or: Si el resultat pot canviar legítimament entre execucions → és validació, no testing.
Què SÍ hem de testejar
1. Validació de dades
Les funcions que validen les dades d’entrada són deterministes i crítiques:
# src/validation.py
def validate_age(age: float) -> bool:
"""Comprova que l'edat és vàlida."""
return 0 <= age <= 120
def validate_income(income: float) -> bool:
"""Comprova que l'ingrés és vàlid."""
return income >= 0
# tests/test_validation.py
def test_validate_age_valid():
assert validate_age(30) == True
assert validate_age(0) == True
assert validate_age(120) == True
def test_validate_age_invalid():
assert validate_age(-5) == False
assert validate_age(150) == False
assert validate_age(float('nan')) == False
Per què: Bugs en la validació poden permetre dades invàlides que trenquen el model silenciosament.
2. Funcions de preprocessament
Les transformacions de dades han de produir resultats correctes:
# src/preprocessing.py
def normalize_value(value: float, min_val: float, max_val: float) -> float:
"""Normalitza un valor entre 0 i 1."""
return (value - min_val) / (max_val - min_val)
# tests/test_preprocessing.py
def test_normalize_value():
assert normalize_value(0, 0, 100) == 0.0
assert normalize_value(100, 0, 100) == 1.0
assert normalize_value(50, 0, 100) == 0.5
3. Càrrega i funcionament bàsic del model
No testem accuracy, però sí que el model es pot carregar i usar:
# tests/test_model.py
import joblib
def test_model_loads():
"""Verifica que el model es pot carregar."""
model = joblib.load('models/model.pkl')
assert model is not None
assert hasattr(model, 'predict')
def test_model_predicts_correct_shape():
"""Verifica que les prediccions tenen la forma correcta."""
model = joblib.load('models/model.pkl')
X = [[30, 50000, 5]] # Exemple amb 3 features
predictions = model.predict(X)
assert len(predictions) == 1
assert isinstance(predictions[0], (int, float))
4. Endpoints de l’API
Els tests d’integració verifiquen que l’API funciona correctament:
# tests/conftest.py
import pytest
from fastapi.testclient import TestClient
from api import app
@pytest.fixture(scope="session")
def client():
# El bloc `with` executa els events lifespan (startup/shutdown)
# scope="session" carrega el model una sola vegada per a tota la suite
with TestClient(app) as c:
yield c
# tests/test_api.py
def test_health_endpoint(client):
"""Verifica que el health check funciona."""
response = client.get("/health")
assert response.status_code == 200
def test_predict_endpoint_valid(client):
"""Verifica predicció amb dades vàlides."""
response = client.post("/predict", json={
"age": 30,
"income": 50000,
"score": 5
})
assert response.status_code == 200
assert "prediction" in response.json()
def test_predict_endpoint_invalid(client):
"""Verifica que dades invàlides són rebutjades."""
response = client.post("/predict", json={
"age": -5, # Invàlid!
"income": 50000
})
assert response.status_code == 422 # Validation error
Per què
with TestClient(app) as c? Si la teva API usa el patrólifespande FastAPI per carregar el model a l’inici, cal entrar al context manager (with) perquè els events de startup s’executin. Sense elwith, el model no es carrega i tots els tests falla.scope="session"carrega el model una sola vegada per a tota la suite de tests, no per a cada test individual.
Què NO hem de testejar
❌ No testejar accuracy o mètriques del model:
# ❌ MAL - Tests inestables!
def test_model_accuracy():
assert model.score(X_test, y_test) > 0.95 # Això fallarà impredictiblement
Per què no: L’accuracy varia amb l’entrenament. Això és validació, no testing. Usa el validation set durant l’entrenament, no tests.
❌ No testejar prediccions específiques:
# ❌ MAL - Model pot canviar legítimament
def test_specific_prediction():
X = [[30, 50000, 5]]
assert model.predict(X)[0] == 1 # Massa específic!
Per què no: Si millorem el model, aquest test podria fallar tot i que el nou model és millor.
❌ No testejar el pipeline d’entrenament complet:
# ❌ MAL - Massa lent i complex
def test_entire_training():
data = load_data() # 5 minuts
model = train(data) # 20 minuts
assert model.score(X_test, y_test) > 0.8 # Massa temps!
Per què no: Els tests han de córrer ràpid (segons, no minuts). Testa components individuals.
Pytest: els fonaments
pytest és l’eina estàndard per testejar en Python. Funciona així:
- Crea fitxers que comencin per
test_dins d’un directoritests/ - Escriu funcions que comencin per
test_ - Usa
assertper comprovar condicions
# Estructura recomanada
project/
├── src/
│ ├── api.py
│ ├── validation.py
│ └── preprocessing.py
├── tests/
│ ├── test_api.py
│ ├── test_validation.py
│ ├── test_preprocessing.py
│ └── test_model.py
└── models/
└── model.pkl
Executar tests:
# Instal·lar pytest
pip install pytest
# Executar tots els tests
pytest tests/
# Executar amb més detall
pytest tests/ -v
# Executar només un fitxer
pytest tests/test_api.py
Fixtures: reutilitzar configuració
Si necessitem configurar coses abans dels tests (carregar un model, crear dades), usem fixtures:
# tests/conftest.py
@pytest.fixture
def model():
return joblib.load('models/model.pkl')
# tests/test_model.py
def test_model_predicts(model): # Fixture injectada automàticament
prediction = model.predict([[30, 50000, 5]])
assert prediction is not None
conftest.py i descoberta automàtica
pytest carrega conftest.py automàticament — no cal cap import als fitxers de test. La injecció funciona per nom de paràmetre: si una funció de test té un paràmetre model, pytest busca un fixture que es digui model i l’injecta. El conftest.py a l’arrel de tests/ és accessible per tots els fitxers de test del directori.
Fixtures integrats de pytest
pytest proporciona fixtures útils sense necessitat d’instal·lar res addicional:
tmp_path: directori temporal únic per a cada test, creat automàticament i eliminat quan el test acaba. Ideal per a tests que escriuen fitxers (logs, prediccions, etc.) sense contaminar el sistema de fitxers real.monkeypatch: permet sobreescriure temporalment atributs, variables d’entorn i funcions durant l’execució d’un test. Al final del test, els valors originals es restauren automàticament.
# Exemple: redirigir el fitxer de log durant un test
@pytest.fixture
def temp_log(tmp_path, monkeypatch):
log_file = tmp_path / "predictions.jsonl"
monkeypatch.setattr("app.pred_store.settings.LOG_PATH", str(log_file))
return log_file
def test_predict_logs(client, temp_log):
client.post("/predict", json={...})
assert temp_log.exists() # El log és al fitxer temporal, no al real
Sense monkeypatch, el test escriuria al fitxer de producció real — un efecte secundari que contaminaria l’entorn fora dels tests.
Scope dels fixtures
Per defecte, un fixture es crea de nou per a cada test (scope="function"). Per a recursos cars (carregar un model, crear un client HTTP), pot ser millor crear-los una sola vegada:
# scope="session": creat una sola vegada per a tota la suite de tests
@pytest.fixture(scope="session")
def client():
with TestClient(app) as c:
yield c # yield en lloc de return permet executar codi de neteja després
# scope="function" (per defecte): creat de nou per a cada test
@pytest.fixture
def sample_data():
return {"age": 30, "income": 50000}
El patró yield és especialment important: el codi abans del yield s’executa en el setup del fixture, i el codi després (si n’hi ha) s’executa en el teardown. Per exemple, with TestClient(app) as c: yield c garanteix que els events de shutdown de l’app s’executen quan la suite acaba.
Resum del testing ML
Testa:
- ✅ Validació de dades
- ✅ Preprocessament
- ✅ Càrrega del model
- ✅ API endpoints
- ✅ Format de les prediccions
NO testis:
- ❌ Accuracy del model
- ❌ Prediccions específiques
- ❌ Pipeline d’entrenament complet
Recorda: Els tests asseguren que la infraestructura funciona. La qualitat del model es valida durant l’entrenament amb el validation set.
Type hints: validació estàtica
A més de tests, podem detectar errors abans d’executar el codi amb type hints i eines com mypy. Els type hints són anotacions de tipus que indiquen què espera cada funció.
Per què són útils en ML:
- Detecció d’errors:
mypydetecta tipus incorrectes abans d’executar - Documentació viva: Els tipus expliquen què espera cada funció
- IDE autocomplete: Millora la productivitat
Quan usar-los:
- ✅ Sempre en funcions públiques (APIs, endpoints)
- ✅ Sempre en funcions de validació i preprocessament
- ⚠️ Opcional en scripts d’experimentació o notebooks
Exemple:
# ✅ Amb type hints
def predict(features: list[float]) -> dict[str, float]:
...
def preprocess(data: pd.DataFrame, columns: list[str]) -> pd.DataFrame:
...
Validació amb mypy:
pip install mypy
mypy src/
# Exemple d'error que detecta:
# error: Argument 1 to "predict" has incompatible type "str"; expected "list[float]"
Configuració recomanada a pyproject.toml:
[tool.mypy]
python_version = "3.11"
ignore_missing_imports = true # Necessari per sklearn, pandas, etc.
Automatització de la validació
Ara que sabem què testejar (validació de dades, preprocessament, càrrega del model, endpoints), necessitem automatitzar quan s’executen aquests tests. Al capítol anterior hem vist els fonaments de CI/CD; ara veurem com els projectes de ML afegeixen passos específics de validació al pipeline.
CI/CD per a projectes ML
Què afegeix ML al pipeline?
Els projectes de ML necessiten passos addicionals que no existeixen en software tradicional:
| Fase | Software tradicional | ML afegeix |
|---|---|---|
| Validació d’entrada | Linting, format | Validació d’schema de dades |
| Build | Compilar, empaquetar | Verificar càrrega del model |
| Tests | Unit, integració | Smoke tests del model |
| Quality gates | Code coverage | Criteris de desplegament (F1, latència, etc.) |
| Artifacts | Docker image | Docker + registre de models |
| Deploy | Blue-green, canary | Shadow mode, champion-challenger |
Connexió important: Els criteris de desplegament que hem definit a la secció anterior (F1 > 0.75, recall > 0.70, latència < 100ms) es converteixen en portes de qualitat automàtiques dins del pipeline CI/CD.
Pipeline CI/CD complet per ML
Diferència clau amb software tradicional: Els passos de validació de dades, criteris de model i registre de model són específics de ML.
Integrar els criteris de desplegament
El script validate_model.py que hem creat a la secció anterior s’integra directament al pipeline:
# Exemple conceptual (GitHub Actions)
jobs:
validate:
steps:
- name: Run tests
run: pytest tests/ -v
- name: Validate model criteria
run: python scripts/validate_model.py
# Falla si el model no compleix F1 > 0.75, etc.
- name: Build Docker
run: docker build -t model:${{ github.sha }} .
Si validate_model.py retorna exit code 1 (model no aprovat), el pipeline s’atura i no es desplega.
Model registry
A més de Docker images, els projectes ML necessiten versionar els models amb les seves mètriques. Un model registry és un repositori centralitzat que guarda:
- L’artefacte del model (fitxer
.pkl,.onnx, etc.) - Mètriques d’avaluació (F1, accuracy, latència)
- Metadades (data d’entrenament, paràmetres, dataset)
- Estat (staging, production, archived)
Per a projectes petits, podem implementar un registre senzill amb git: models versionats, fitxers JSON amb mètriques, i tags per marcar versions. Per a projectes més grans, existeixen eines especialitzades com MLflow, DVC o Weights & Biases.
Les estratègies de desplegament específiques per a ML (shadow mode, canary, A/B testing) es tracten en detall al capítol d’aprenentatge continu.
Implementació progressiva
No cal implementar-ho tot de cop:
- Fase 1: Validació manual (executar tests i
validate_model.pyabans de desplegar) - Fase 2: Git hooks locals per feedback ràpid
- Fase 3: CI bàsic (tests + build Docker)
- Fase 4: CI complet (data validation + model gates + model registry)
- Fase 5: CD automatitzat amb estratègies de desplegament
Git hooks: automació per self-hosted
Els git hooks són una tècnica fonamental de CI/CD, especialment en entorns self-hosted on controlem la infraestructura. Són scripts que s’executen automàticament en resposta a esdeveniments de git.
Tipus de hooks
Client-side hooks (al repositori local del desenvolupador):
pre-commit: S’executa abans de crear un commitpre-push: S’executa abans de fer push
Server-side hooks (al servidor git):
pre-receive: S’executa abans d’acceptar un pushpost-receive: S’executa després d’acceptar un push (desplegament!)update: S’executa per cada branch que es pugi
Pre-commit hook: validació local
El pre-commit hook s’executa automàticament abans de crear un commit al repositori local. El seu propòsit és donar feedback immediat al desenvolupador.
Què fa:
- S’executa automàticament quan fas
git commit - Executa validacions ràpides (tests, linters, format)
- Si alguna validació falla, rebutja el commit
- Si tot va bé, permet crear el commit
Flux conceptual:
git commit -m "fix bug"
↓
[Hook executa tests automàticament]
↓
Tests OK? ──┬─ SÍ → Commit creat ✓
└─ NO → Commit REFUSAT ✗
(el desenvolupador veu l'error immediatament)
Avantatge principal: Feedback en segons, abans que el codi surti del teu ordinador.
Limitació important: El desenvolupador pot saltar-se’l amb git commit --no-verify. Per això, la validació definitiva ha d’estar al servidor, no només al client.
Post-receive hook: desplegament automàtic
El post-receive hook és el cor del CI/CD en entorns self-hosted. S’executa al servidor git després d’acceptar un push.
Què fa:
- S’executa automàticament quan algú fa
git pushal servidor - Típicament només s’activa per la branch principal (
main/master) - Executa tot el pipeline de validació i desplegament
- Si qualsevol pas falla, aborta el desplegament
Pipeline típic:
- Checkout: Actualitza el codi en el directori de treball del servidor
- Test: Executa els tests en un entorn aïllat (per exemple, amb
docker-compose.validate.yml) - Validació del model: Verifica que el model compleix criteris mínims de qualitat
- Build i desplegament: Construeix i aixeca els contenidors de producció
- Health check: Verifica que l’API respon correctament
Si qualsevol d’aquests passos falla, el desplegament s’atura i el desenvolupador rep l’error.
Flux conceptual:
Organització de hooks en projectes grans
En projectes més complexos, és recomanable modularitzar els hooks separant cada fase del pipeline en scripts independents:
Fases típiques:
- Validació: Executar tests automàtics
- Pre-desplegament: Validar que el model compleix criteris de qualitat
- Desplegament: Aixecar els nous contenidors/serveis
- Post-desplegament: Verificar que tot funciona (health checks)
Avantatge de la modularització:
- Cada script té una responsabilitat única
- És més fàcil debugar quan alguna fase falla
- Es poden reutilitzar scripts en diferents contextos
- El hook principal només orquestra l’execució seqüencial
El hook principal (per exemple, post-receive) simplement crida cada script en ordre, i si qualsevol falla, atura tot el pipeline.
Quan usar git hooks vs. plataformes CI/CD?
Git hooks són ideals quan:
- Controlem el servidor (self-hosted)
- Volem solució simple sense dependències externes
- Projectes petits-mitjans
- Desplegaments a un sol servidor
- Necessitem automatització immediata
Plataformes CI/CD (GitHub Actions, GitLab CI) quan:
- Repositoris públics o cloud
- Múltiples entorns (staging, production, diferents clouds)
- Projectes grans amb múltiples equips
- Necessitem workflows complexos (matriu de tests, múltiples SO)
- Volem UI visual del pipeline
Combinació: Poden conviure! Pre-commit local + post-receive al servidor + GitHub Actions per tests addicionals.
Bones pràctiques
- Idempotència: Els scripts han de poder executar-se múltiples vegades sense problemes
- Logs clars: Missatges descriptius de què està passant
- Exit codes:
exit 0si èxit,exit 1si error - Timeouts: Evitar que hooks pengin indefinidament
- Rollback: Estratègia per revertir desplegaments fallits
Resum: Doble capa de protecció
| Validació local (pre-commit) | Validació remota (post-receive) |
|---|---|
| Feedback ràpid (segons) | Feedback després del push (minuts) |
| Es pot saltar (–no-verify) | Impossible saltar |
| Detecta errors abans del push | Detecta errors abans del desplegament |
| Configurat per cada desenvolupador | Configurat centralment al servidor |
| Primera línia de defensa | Última línia de defensa |
Junts, garanteixen que el codi erroni no arribi mai a producció!
Optimització en producció
Quan passem d’un notebook a un sistema en producció, un dels reptes principals és la velocitat d’inferència: quant triga el model a respondre. Les tècniques més importants per reduir latència i consum de recursos són:
Reduir la mida i el cost del model
Aquestes tècniques fan el model més lleuger sense alterar gaire la seva qualitat:
-
Quantització: Fer els números més petits.
- Consisteix a representar els pesos del model amb menys bits.
- Això redueix memòria, temps de càlcul i sovint té un impacte molt petit en la precisió.
- Exemples:
float32 → float16: la meitat d’espai, gairebé mateixa qualitatfloat32 → int8: un 75% menys d’espai, rendiment molt millor
- Útil quan la latència és crítica (mòbils, edge, real-time).
-
Poda (Pruning): Treure neurones que fan poca feina.
- Elimina parts del model que aporten molt poc.
- Imaginem una xarxa com un arbre: la poda treu branques inútils.
- Menys pesos → càlcul més ràpid.
- L’impacte en precisió sol ser petit si es fa progressivament.
-
Destil·lació (Knowledge Distillation): Un model gran ensenya a un de petit com comportar-se.
- El model “professor” genera sortides suaus.
- Un model “estudiant” més petit aprèn a imitar-lo.
- Permet mantenir gairebé el mateix rendiment amb un model molt més lleuger.
-
Factorització de baix rang (Low-rank): Descompondre matrius grans en peces més petites.
- Divideix matrius de pesos en dues o tres matrius petites.
- Mantenim quasi el mateix comportament amb menys càlcul.
Optimitzar el flux intern del model
Millorem com el model “fa les coses per dins”:
-
Fusió d’operacions (Operator Fusion): Fer diverses operacions en una sola passa.
- Evita moviments de memòria i passos innecessaris.
- Per exemple, combinar Convolució + BatchNorm + ReLU.
- Runtimes com ONNX Runtime, TensorRT o OpenVINO ho fan automàticament.
-
Optimització del graf (Graph Optimization): Netejar i reordenar les operacions.
- Inclou:
- treure nodes inútils
- simplificar expressions
- refactoritzar el flux d’operacions
- S’activa sovint en exportar a ONNX.
- Inclou:
-
Compilació específica per hardware: Generar codi optimitzat per la màquina on s’executarà.
- Exemples:
- TorchScript (PyTorch)
- XLA (TensorFlow)
- TVM (compilador ML open source)
- TensorRT (NVIDIA, molt ràpid en GPU)
- Exemples:
Escollir millors models i millor arquitectura
A vegades la millor optimització és utilitzar un model més eficient:
-
Models dissenyats per ser lleugers: Arquitectures pensades per ser ràpides.
- Exemples coneguts:
- MobileNet (visió)
- EfficientNet-Lite (visió)
- DistilBERT / TinyBERT (text)
- Whisper-Tiny (àudio)
- Exemples coneguts:
-
Selecció de features: Menys característiques = menys feina.
- El pre-processament pot ser la part més lenta del pipeline.
- Reduir features també redueix latència.
Estratègies de serving i producció
Maneres de millorar el rendiment al desplegar el model:
-
Batching dinàmic: Agrupar peticions en mini-lots per aprofitar millor el hardware.
- Permet fer 10 inferències en una sola passa en comptes de 10 passes separades.
- Eines que ho ofereixen:
- NVIDIA Triton
- Ray Serve
- KServe
- BentoML
-
Paral·lelització i multithreading: Aprofitar diversos nuclis de CPU.
- Podeu servir múltiples peticions en paral·lel amb diversos workers o processos.
-
Caching d’inferències: Si una petició es repeteix, no cal recalcular.
- Desa resultats en memòria (Redis/Memcached).
- Per a moltes aplicacions, és una de les optimitzacions més potents.
Millorar l’aprofitament del hardware
Per millorar l’aprofitament del hardware:
-
GPU acceleration: Les GPUs estan fetes per multiplicar matrius molt ràpid.
- Ideal per models grans i procesaments paral·lels.
-
Instruccions vectorials de CPU: Fer més operacions per instrucció.
- Si el servidor suporta AVX/AVX2/AVX-512, el model anirà força més ràpid.
-
Hardware especialitzat: Dispositius creats només per fer inferència.
- Google TPU
- AWS Inferentia
- Intel Movidius (edge)
Resum final
| Acció | Tècniques principals |
|---|---|
| Reduir el model | quantització, poda, destil·lació, low-rank |
| Optimitzar el model | fusió d’operacions, optimització del gràfic, compilació |
| Escollir arquitectura | models lleugers, menys features |
| Millorar el serving | batching, paral·lelització, caching |
| Aprofitar hardware | GPU, AVX, dispositius especialitzats |
Resum
En aquest capítol hem après a garantir la qualitat dels nostres models:
- Definir criteris objectius (tècnics, de negoci, operacionals) per aprovar desplegaments
- Testejar la infraestructura ML sense testejar el model mateix
- Usar git hooks per validació local automàtica abans de commits
- Implementar CI/CD per validació al servidor abans de desplegaments
- Aplicar tècniques d’optimització quan sigui necessari
Amb aquestes pràctiques, tenim control complet sobre la qualitat dels models desplegats. Al proper capítol veurem com monitoritzar models en producció i detectar quan les dades canvien (drift).
Drifting i monitoratge
- Introducció
- Per què les dades canvien?
- Tipus de drift
- Detecció de drift amb mètodes estadístics
- Monitoratge de qualitat de dades
- Monitoratge en producció
- Observabilitat (Observability)
- Establint alertes
- Resum
Introducció
Quan un model de machine learning arriba a producció, el nostre treball no s’acaba: tot just comença una nova fase. El món real és dinàmic, i les dades que el model veu en producció poden ser molt diferents de les que va veure durant l’entrenament. Aquest fenomen s’anomena canvi en la distribució de dades (data distribution shift o simplement drift).
En aquest capítol explorarem els diferents tipus de drift, com detectar-los, i com establir un sistema de monitoratge que ens permeti saber si el nostre model segueix funcionant correctament.
Per què les dades canvien?
Imaginem que hem entrenat un model per predir quins productes comprarà un client. El model va funcionar molt bé durant les proves, però després d’uns mesos en producció, les prediccions ja no són tan bones. Què ha passat?
Les causes poden ser múltiples:
- Canvis en el comportament dels usuaris: Nous hàbits de consum, modes, temporades
- Canvis en el mercat: Nous competidors, canvis de preus, situació econòmica
- Canvis en la recollida de dades: Actualitzacions en l’aplicació, nous camps, errors en sensors
- Canvis en la població: Nous segments de clients, expansió a nous mercats
El problema és que el model va aprendre patrons d’unes dades concretes, i quan aquestes dades canvien, el model pot quedar obsolet.
Tipus de drift
No tots els canvis en les dades són iguals. Distingim tres tipus principals:
Covariate Shift (Canvi en les covariables)
El covariate shift es produeix quan la distribució de les variables d’entrada (features) canvia, però la relació entre les entrades i la sortida es manté.
Exemple: Un model per detectar correu brossa entrenat amb correus d’oficina. Quan l’empresa comença a rebre més correus de màrqueting (nova distribució d’entrades), el model pot fallar perquè mai havia vist aquest tipus de correus, tot i que la definició de “brossa” no ha canviat.
Com detectar-ho: Comparar la distribució de les features entre entrenament i producció.
Label Shift (Canvi en les etiquetes)
El label shift es produeix quan la distribució de les sortides (etiquetes) canvia.
Exemple: Un model de diagnòstic mèdic entrenat amb dades on el 5% dels pacients tenien la malaltia. Si en producció aquest percentatge puja al 20% (per exemple, durant un brot), el model pot tenir problemes perquè els seus llindars de decisió estaven calibrats per a una prevalença diferent.
Entrenament: 95% sans, 5% malalts
Producció: 80% sans, 20% malalts ← El model pot subestimar casos positius
Com detectar-ho: Monitoritzar la distribució de les prediccions (si no tenim ground truth, és a dir, les etiquetes reals verificades) o del ground truth (si l’obtenim amb retard).
Concept Drift (Canvi de concepte)
El concept drift és el més problemàtic: la relació entre les entrades i la sortida canvia.
Exemple: Un model per predir si un client pagarà un préstec, entrenat abans d’una crisi econòmica. Després de la crisi, clients que abans eren bons pagadors ara no poden pagar. Les mateixes features (ingressos, edat, historial) ara porten a resultats diferents.
Abans: Ingressos alts + Historial bo → Alta probabilitat de pagar
Després: Ingressos alts + Historial bo → Probabilitat incerta (crisi!)
Com detectar-ho: Només es pot detectar amb certesa si tenim el ground truth de producció. Altrament, podem inferir-ho si detectem degradació en les mètriques del model.
Què podem detectar amb cada enfocament?
| Tipus de drift | Com detectar-lo |
|---|---|
| Covariate shift | Tests estadístics sobre les features (veure secció següent) |
| Label shift | Monitoritzar la distribució de les prediccions al llarg del temps |
| Concept drift | Necessita ground truth — comparar prediccions vs resultats reals |
Els mètodes estadístics que veurem a continuació estan dissenyats principalment per detectar covariate shift. Per detectar concept drift cal monitoritzar les mètriques del model amb ground truth, cosa que no sempre és possible immediatament.
Detecció de drift amb mètodes estadístics
Per detectar drift, comparem distribucions: les dades d’entrenament (o d’un període de referència) contra les dades actuals de producció. Hi ha diversos mètodes estadístics per fer-ho.
Tests de dues mostres (Two-sample tests)
Aquests tests responen a la pregunta: “Aquestes dues mostres provenen de la mateixa distribució?”
Test de Kolmogorov-Smirnov (KS)
El test KS mesura la màxima diferència entre les funcions de distribució acumulada de dues mostres. És útil per a variables numèriques contínues.
from scipy import stats
# Per cada feature numèrica, comparar distribucions
statistic, p_value = stats.ks_2samp(reference_data['age'], current_data['age'])
if p_value < 0.05:
print(f"Drift detectat (p={p_value:.4f})")
Interpretació: Un p-valor baix (típicament < 0.05) indica que les dues mostres probablement no vénen de la mateixa distribució.
Per a variables categòriques, el test chi-quadrat (scipy.stats.chisquare) compara les freqüències observades amb les esperades.
Population Stability Index (PSI)
El PSI és una mètrica molt utilitzada en la indústria financera per mesurar quant ha canviat una distribució.
import numpy as np
def calculate_psi(reference, current, bins: int = 10, smoothing: float = 1e-4) -> float:
"""
Calculate Population Stability Index.
Returns PSI value: < 0.1 insignificant, 0.1-0.2 investigate, > 0.2 action required.
"""
# Crear bins basats en la distribució de referència
breakpoints = np.percentile(reference, np.linspace(0, 100, bins + 1))
breakpoints[0] = -np.inf
breakpoints[-1] = np.inf
# Calcular proporcions per cada bin
ref_counts = np.histogram(reference, bins=breakpoints)[0]
cur_counts = np.histogram(current, bins=breakpoints)[0]
ref_pct = ref_counts / len(reference)
cur_pct = cur_counts / len(current)
# Evitar divisió per zero
ref_pct = np.where(ref_pct == 0, smoothing, ref_pct)
cur_pct = np.where(cur_pct == 0, smoothing, cur_pct)
# Calcular PSI
psi = np.sum((cur_pct - ref_pct) * np.log(cur_pct / ref_pct))
return psi
Interpretació del PSI:
- PSI < 0.1: Canvi insignificant
- 0.1 ≤ PSI < 0.2: Canvi moderat, investigar
- PSI ≥ 0.2: Canvi significatiu, acció requerida
Finestres de temps
Per aplicar aquests tests en producció, necessitem definir finestres de temps (time windows):
- Finestra de referència: Dades d’entrenament o d’un període estable
- Finestra actual: Dades recents de producció (últimes hores, dies, o setmanes)
Simular drift amb dades estàtiques
Un repte comú durant el desenvolupament és: com testem la nostra detecció de drift si encara no tenim dades de producció? La resposta és simular drift artificialment amb dades estàtiques.
⚠️ Tècnica de desenvolupament: Aquesta és una tècnica per a desenvolupament i testing, no una pràctica de producció. En sistemes reals de producció, la detecció de drift es fa amb dades que arriben contínuament de tràfic real, no amb datasets estàtics pre-dividits. Simular drift és útil per validar el codi abans de desplegar-lo.
Per què simular drift?
Durant la fase de desenvolupament i testing:
- No tenim dades de producció encara
- Volem verificar que el nostre codi de detecció funciona correctament
- Necessitem provar diferents escenaris de drift
- Volem establir llindars abans de desplegar
Simular drift no és fer trampes - és una tècnica estàndard per desenvolupar i validar sistemes de monitoratge.
Estratègia: Modificar features artificialment
La idea central és crear un drift conegut per verificar que el nostre sistema de detecció funciona correctament. Agafem un conjunt de dades i l’alterem deliberadament de formes específiques.
Tipus de modificacions comunes:
-
Shift additiu (covariate drift numèric)
- Exemple: Augmentar l’edat de tots els registres en 5 anys
- Simula: Envelliment de la població, canvis generacionals
-
Shift multiplicatiu (canvis d’escala)
- Exemple: Multiplicar els preus per 1.3
- Simula: Inflació, canvis en unitats de mesura
-
Canvis en distribucions categòriques
- Exemple: Canviar 30% dels registres de “urbà” a “rural”
- Simula: Canvis demogràfics, preferències d’usuaris
-
Introducció d’outliers
- Exemple: Afegir valors extrems en algunes features
- Simula: Esdeveniments inusuals, errors de dades
Workflow de testing amb drift simulat
El procés típic per validar un sistema de detecció:
- Preparar dades de referència: Usar el training set o un batch inicial conegut
- Crear escenaris de drift: Modificar una còpia de les dades de diferents formes
- Executar detecció: Aplicar els tests estadístics (KS, PSI, chi-square) a cada escenari
- Verificar resultats:
- Casos sense drift → El sistema NO hauria de disparar alarmes
- Casos amb drift lleu → Depèn dels llindars configurats
- Casos amb drift fort → El sistema SÍ hauria de detectar-lo
Aquest workflow es pot automatitzar amb tests unitaris que verifiquen el comportament esperat.
Escenaris de drift per validar
És recomanable testejar diferents intensitats de drift per calibrar llindars:
| Intensitat | Exemple de modificació | Objectiu del test |
|---|---|---|
| Cap drift | Dades originals | Verificar que NO genera falsos positius |
| Shift lleu | Canvis petits (age + 2) | Verificar tolerància a variació normal |
| Shift moderat | Canvis mitjans (age + 5, income × 1.15) | Ajustar llindars d’alerta |
| Shift fort | Canvis grans (age + 15, price × 2) | Verificar detecció clara |
Aquesta gradació ajuda a establir llindars abans de desplegar a producció: on posem el límit entre “variació acceptable” i “drift que requereix acció”?
Quan usar aquesta tècnica?
Usar simulació de drift per:
- ✅ Desenvolupar i debugar codi de detecció de drift
- ✅ Establir llindars abans de producció
- ✅ Tests automatitzats de la lògica de detecció
- ✅ Entrenar l’equip sobre què esperar en producció
- ✅ Validar que les alertes funcionen correctament
NO substitueix:
- ❌ Monitoratge real de producció (necessitem dades reals!)
- ❌ Anàlisi de causes de drift (simulació no explica per què passa)
- ❌ Decisions de reentrenament (basades en impacte real, no simulat)
Conclusió: Simular drift és una eina de desenvolupament i validació, no de producció. Un cop desplegat, cal monitoritzar dades reals.
Monitoratge de qualitat de dades
La detecció de drift és essencial, però no és l’única cosa que hem de monitoritzar. Les dades poden tenir problemes de qualitat que afecten el model sense que hi hagi drift estadístic.
Tipus de problemes de qualitat
1. Integritat de les dades
- Valors nuls: percentatge de missing values per feature
- Duplicats: registres repetits
- Violacions d’esquema: tipus de dades incorrectes, columnes inesperades
2. Qualitat de les features
- Outliers: valors fora del rang esperat
- Violacions de rang: valors fora dels límits coneguts
- Canvis de cardinalitat: noves categories en features categòriques
3. Qualitat de les prediccions
- Distribució de confiança: les prediccions són menys segures?
- Calibratge: les probabilitats predites coincideixen amb les freqüències reals?
Què validar?
- Valors nuls: Percentatge de missing values per feature
- Outliers: Valors a més de 3 desviacions estàndard
- Noves categories: Categories no vistes durant l’entrenament
- Violacions de rang: Valors fora dels límits esperats (edat negativa, etc.)
La validació s’hauria de fer abans d’usar les dades per entrenar o servir prediccions. Si es detecten problemes crítics, el pipeline hauria d’avortar i alertar.
Diferència clau: El drift mesura canvis en distribucions; la qualitat mesura problemes en les dades mateixes.
Monitoratge en producció
Detectar drift és només una part del monitoratge. Un sistema de monitoratge complet ha de vigilar múltiples aspectes.
Emmagatzematge de prediccions per monitoratge
Abans de poder monitoritzar el nostre model, necessitem guardar les prediccions que fem en producció. Sense aquest registre, no podem detectar drift, calcular mètriques, ni analitzar el comportament del model.
Què cal guardar?
Per a cada predicció, hauríem de registrar:
Essencial:
- Timestamp: Quan s’ha fet la predicció
- Input features: Les dades d’entrada (sense informació sensible!)
- Prediction: La sortida del model
- Request ID: Identificador únic de la petició (opcional però útil)
Recomanat:
- Model version: Quina versió del model ha fet la predicció
- Confidence/probability: Si el model retorna probabilitats
- Latency: Temps que ha trigat la predicció
Opcional:
- User ID: Si és rellevant i permet privacitat
- Context: Informació addicional del context de la petició
On guardar les prediccions?
Tenim diverses opcions, depenent de l’escala i complexitat:
Fitxers de log JSON (recomanat per començar):
import json, logging
from datetime import datetime
pred_logger = logging.getLogger('predictions')
pred_logger.addHandler(logging.FileHandler('logs/predictions.jsonl'))
def log_prediction(inputs: dict, prediction: float, model_version: str, latency: float):
entry = {
'timestamp': datetime.now().isoformat(),
'inputs': inputs,
'prediction': prediction,
'model_version': model_version,
'latency_ms': latency * 1000
}
pred_logger.info(json.dumps(entry))
Això genera un fitxer predictions.jsonl amb una línia JSON per predicció, fàcil d’analitzar amb pandas o scripts.
Per a volums més alts, es pot usar SQLite (queries SQL ràpides) o PostgreSQL (escalable, concurrent).
Consideracions de privacitat i seguretat
⚠️ Molt important: No guardar dades sensibles sense anonimitzar!
def anonymize_inputs(inputs: dict) -> dict:
"""Elimina o anonimitza dades sensibles."""
safe_inputs = inputs.copy()
# Eliminar camps sensibles
sensitive_fields = ['email', 'phone', 'ssn', 'credit_card']
for field in sensitive_fields:
if field in safe_inputs:
safe_inputs[field] = '***REDACTED***'
# Hash d'identificadors si cal mantenir-los
if 'user_id' in safe_inputs:
safe_inputs['user_id'] = hashlib.sha256(
str(safe_inputs['user_id']).encode()
).hexdigest()[:16]
return safe_inputs
# Usar abans de guardar
safe_inputs = anonymize_inputs(raw_inputs)
log_prediction(safe_inputs, prediction, model_version, latency)
Resum: estratègia de logging
Per començar (projectes petits):
- ✅ Logs JSON en fitxers (
predictions.jsonl) - ✅ Guardar: timestamp, inputs, prediction, model_version
- ✅ Analitzar amb scripts Python/pandas periòdicament
Per escalar (projectes mitjans):
- ✅ SQLite per emmagatzematge més eficient
- ✅ Índexs per timestamps per queries ràpides
- ✅ Scripts automatitzats (cron) per analitzar diàriament
Per producció (projectes grans):
- ✅ PostgreSQL o base de dades distribuïda
- ✅ Anonymització automàtica de dades sensibles
- ✅ Backups automàtics i retenció de dades
- ✅ Monitoratge en temps real amb alertes
Amb prediccions guardades, podem començar a detectar drift i monitoritzar el model!
Columnes de Predicció: Per Què i Com
Ara que sabem què guardar i on guardar-ho, entenguem per què cada columna és important i com s’utilitza en monitoratge i debugging.
Columnes i els Seus Casos d’Ús
Columnes Essencials:
- Timestamp: Anàlisi temporal i finestres de comparació (última setmana vs. anterior)
- Features: Detectar covariate shift (canvis en distribució d’entrades) i reproducibilitat
- Prediction: Detectar concept drift indirecte (canvis en distribució de sortides)
- Request ID: Debugging i traceability (correlacionar errors amb prediccions específiques)
Columnes Recomanades:
- Model Version: Comparar rendiment entre versions, rollback decisions, A/B testing
- Probability: Calibration monitoring i early warning de drift (canvis en confidence)
- Latency: Performance monitoring i SLA compliance
Columnes Opcionals:
- User ID: Personalització i bias detection (requereix anonymització per GDPR)
Casos d’Ús i Columnes Necessàries:
| Cas d’Ús | Columnes Clau |
|---|---|
| Detectar Covariate Shift | Timestamp + Features |
| Detectar Concept Drift | Timestamp + Prediction + Probability |
| Debugging | Request ID + Features + Model Version |
| A/B Testing | Model Version + Prediction + Timestamp |
| Calibration Monitoring | Probability + Ground Truth (quan disponible) |
Què monitoritzar?
1. Mètriques del model (Model metrics)
Si tenim accés al ground truth (encara que sigui amb retard), podem calcular les mètriques tradicionals: accuracy, precision, recall, F1 per classificació; MSE, MAE, R² per regressió.
2. Distribució de les prediccions
Fins i tot sense etiquetes, podem monitoritzar la distribució de les prediccions:
- Per classificació: Proporció de cada classe predita
- Per regressió: Mitjana, mediana, rang de les prediccions
Si la distribució de prediccions canvia dràsticament, pot ser senyal de drift.
3. Distribució de les features
Monitoritzar les features d’entrada ens permet detectar covariate shift. Si la mitjana d’una feature actual està a més de N desviacions estàndard de la referència, cal investigar.
4. Mètriques operacionals
A més del model, hem de monitoritzar el sistema:
- Latència: Temps de resposta de l’API (p50, p95, p99)
- Throughput: Nombre de peticions per segon
- Errors: Taxa d’errors (5xx, timeouts)
- Recursos: Ús de CPU, memòria, disc
Arquitectura de monitoratge simple
Per al nostre context (scripts Python, sense dependències complexes), podem implementar un sistema de monitoratge basat en:
- Logging estructurat: Registrar tot en fitxers de log amb format consistent
- Scripts d’anàlisi: Scripts que processen els logs periòdicament
- Visualització simple: Gràfics amb matplotlib o similar
Un script d’anàlisi periòdic (cron job diari) pot llegir els logs JSON, calcular estadístiques (latència mitjana, p95, taxa d’errors), i generar alertes si es superen llindars.
Observabilitat (Observability)
El monitoratge ens diu què passa (mètriques, alertes), però l’observabilitat ens permet entendre per què passa.
Els tres pilars de l’observabilitat
- Mètriques (Metrics): Valors numèrics agregats (latència mitjana, taxa d’errors)
- Logs: Registres detallats d’esdeveniments individuals
- Traces: Seguiment del camí d’una petició a través del sistema
Per al nostre context simplificat, ens centrarem en logs ben estructurats que ens permetin fer anàlisi posterior.
Logs estructurats
En lloc de logs de text lliure, utilitzar format JSON facilita l’anàlisi posterior. Cada línia de log hauria de contenir: timestamp, event type, dades rellevants (inputs anonimitzats, prediction, latency, model_version), i informació de context (request_id).
{"timestamp": "2024-01-15T10:30:45", "event": "prediction", "request_id": "abc123", "prediction": 0.85, "latency_ms": 45.2, "model_version": "1.2.0"}
Establint alertes
Les alertes ens notifiquen quan alguna cosa requereix atenció. La clau és trobar l’equilibri: prou sensibles per detectar problemes, però no tant que generin fatiga d’alertes.
Tipus d’alertes
-
Alertes de llindar (Threshold alerts): Quan una mètrica supera un valor fix
- Latència > 500ms
- Taxa d’errors > 1%
- PSI > 0.2
-
Alertes d’anomalia: Quan una mètrica es desvia significativament del seu comportament normal
- Nombre de peticions 50% inferior a la mitjana de la mateixa hora
-
Alertes de tendència: Quan una mètrica mostra una tendència preocupant
- Latència augmentant progressivament durant 3 dies
Implementació d’alertes
Les alertes es basen en comparar mètriques contra llindars predefinits:
| Mètrica | Llindar típic | Severitat |
|---|---|---|
| Latència p95 | > 500ms | warning |
| Taxa d’errors | > 1% | critical |
| PSI (drift) | > 0.2 | warning |
Quan es supera un llindar, es registra l’alerta a un fitxer o s’envia per email/Slack.
Resum
En aquest capítol hem après:
- El drift (canvi en la distribució de dades) és inevitable en producció i pot degradar el rendiment del model
- Hi ha tres tipus principals: covariate shift (canvi en features), label shift (canvi en distribució d’etiquetes), i concept drift (canvi en la relació entre features i etiquetes)
- Podem detectar drift amb tests estadístics com Kolmogorov-Smirnov, chi-quadrat, o el Population Stability Index (PSI)
- Un sistema de monitoratge complet vigila mètriques del model, distribució de prediccions i features, i mètriques operacionals
- L’observabilitat (especialment logs estructurats) ens permet entendre per què passen els problemes
- Les alertes ens notifiquen quan cal actuar, però cal calibrar-les per evitar fatiga
Al proper capítol veurem què fer quan detectem que el model necessita ser actualitzat: l’aprenentatge continu i les estratègies per testar models en producció.
Aprenentatge continu
- Introducció
- Per què actualitzar models?
- Iteració de dades vs. iteració de model
- Amb quina freqüència actualitzar?
- Paradigmes d’actualització: Batch, Streaming, i Continual Learning
- Casos d’ús i estratègies de re-entrenament
- Entrenament amb estat vs. sense estat
- Automatització del pipeline d’actualització
- Test en producció
- Consideracions pràctiques
- Resum
Introducció
Al capítol anterior hem vist com detectar quan les dades canvien i el model comença a degradar-se. La pregunta natural és: què fem quan això passa? La resposta és actualitzar el model, però això obre tot un conjunt de nous reptes.
L’aprenentatge continu (continual learning) és el procés d’actualitzar models en producció per adaptar-se a les dades canviants. No es tracta simplement de re-entrenar de tant en tant, sinó de crear un sistema que pugui evolucionar de manera fiable i segura.
En aquest capítol explorarem quan i com actualitzar models, i les estratègies per provar models nous en producció sense arriscar el servei.
Per què actualitzar models?
Recordem els tipus de drift que hem vist:
- Covariate shift: Les features canvien, cal que el model s’adapti a nous patrons d’entrada
- Label shift: La proporció de classes canvia, cal recalibrar
- Concept drift: La relació entre features i sortida canvia, el model ha après coses que ja no són certes
A més del drift, hi ha altres raons per actualitzar:
- Més dades disponibles: Hem acumulat més exemples que poden millorar el model
- Noves features: Tenim accés a dades noves que poden ser predictives
- Correccions de bugs: Hem descobert errors en el preprocessament o l’entrenament
- Millores algorítmiques: Volem provar un algoritme o arquitectura millor
Iteració de dades vs. iteració de model
Quan el rendiment del model degrada, tenim dues estratègies diferents que sovint es confonen:
Iteració de dades (Data Iteration)
Re-entrenar el model amb dades noves, mantenint l’arquitectura.
- Mateixa arquitectura de model i features
- Afegir mostres noves (nous patrons, concept drift)
- Exemple: Model de frau re-entrenat setmanalment amb nous casos de frau (mateixa arquitectura)
Iteració de model (Model Iteration)
Canviar l’arquitectura o afegir noves features.
- Afegir noves features (noves fonts de dades)
- Canviar arquitectura (algoritme diferent, més complexitat)
- Ajustar hiperparàmetres
- Exemple: Model de crèdit que afegeix “historial laboral” O canvia de regressió logística a XGBoost
Quan usar cada estratègia?
| Símptoma | Causa Probable | Estratègia |
|---|---|---|
| Model funciona bé inicialment, degrada amb el temps | Concept drift | Iteració de dades |
| Model mai arriba a la performance objectiu | Model massa simple | Iteració de model |
| Bo en entrenament, dolent en producció | Distribution shift | Iteració de dades |
| Performance s’estanca malgrat més dades | Model saturat o features insuficients | Iteració de model |
| Noves fonts d’informació disponibles | Nous senyals predictius | Iteració de model |
Implicacions en el pipeline
Pipeline d’iteració de dades:
- Recollir dades noves amb ground truth — el resultat real que confirma si la predicció era correcta (veure De ground truth a dataset d’entrenament per al procés detallat)
- Validar qualitat de dades
- Re-entrenar mateixa arquitectura amb dades noves/actualitzades
- Mantenir features i hiperparàmetres
- Desplegar si la validació passa
Pipeline d’iteració de model:
- Fixar datasets d’entrenament/validació/test (versionats!)
- Afegir noves features O canviar arquitectura
- Entrenar i comparar candidats sobre el mateix test set
- Seleccionar millor configuració
- Re-entrenar amb totes les dades i desplegar
Enfocament híbrid (més comú en producció)
- Iteració de dades regular (setmanal/mensual) → manté el model fresc
- Iteració de model ocasional (trimestral/anual) → millora capacitat fonamental
Relació amb valor de negoci:
- Iteració de dades → s’adapta al món canviant (patrons de frau evolucionen)
- Iteració de model → millora capacitat fonamental (nous senyals, millor arquitectura)
Aplicar continual learning amb datasets estàtics
Fins ara hem parlat de conceptes com si tinguéssim un flux continu de dades. Però durant l’aprenentatge i desenvolupament, sovint treballem amb datasets estàtics. Com apliquem aquests conceptes?
⚠️ Simulació per a aprenentatge: Aquesta secció mostra com practicar continual learning amb datasets estàtics. En producció real, els sistemes utilitzen flux continu de dades reals. Els splits estàtics són només per a aprenentatge i validació de la infraestructura. Per a simulació de drift amb dades estàtiques (modificar features artificialment per validar codi de detecció), consulteu el capítol de monitoratge.
Estratègia: Dividir el dataset en fases temporals
Quan treballem amb un dataset fix, la solució és dividir-lo de manera que simuli l’arribada de dades en el temps.
La idea és tractar diferents parts del dataset com si fossin moments diferents en el temps:
Divisió típica:
- Training Set (60%): Entrenar el model inicial
- Validation Set (20%): Ajustar hiperparàmetres i avaluar
- Production Set (20%): Subdividir en múltiples batches que simulen l’arribada de dades en el temps
Subdivisió del Production Set:
- Batch 1: Primeres “dades de producció” → establir baseline de rendiment
- Batch 2: “Dades noves” que arriben després → detectar drift, decidir si reentrenar
- Batch 3, 4…: Cicles addicionals per simular evolució temporal
Workflow conceptual de continual learning simulat
Fase 1: Entrenament inicial
- Entrenar model amb training set
- Validar amb validation set
- Guardar model inicial (versió 1.0.0)
Fase 2: Producció inicial (Batch 1)
- “Desplegar” el model
- Fer prediccions sobre Batch 1
- Calcular mètriques de referència (baseline)
- Guardar aquestes mètriques per comparació futura
Fase 3: Arribada de “dades noves” (Batch 2)
- Simular pas del temps: apareix Batch 2
- Detectar drift: Comparar distribució de Batch 2 vs Batch 1
- Avaluar degradació: El model manté el rendiment en Batch 2?
- Si hi ha degradació significativa → considerar reentrenament
Fase 4: Decisió de reentrenament
- Criteri: Si rendiment cau més d’un llindar (ex: 5% de degradació)
- Estratègies:
- Reentrenar des de zero amb training + Batch 1
- Fine-tuning del model existent amb Batch 1
- Validació: El nou model millora en Batch 2?
- Decisió: Si millora → desplegar nova versió; si no → mantenir model actual
Cicles addicionals:
- Repetir el procés amb Batch 3, 4, etc.
- Cada cicle pot decidir si reentrenar o no
- Acumulant dades: training + batch1 + batch2 + …
- Gestionant versions: v1.0.0 → v1.1.0 → v1.2.0
Limitacions
Aquesta simulació és útil per aprendre, prototipar pipelines i validar estratègies de reentrenament. Però no pot replicar aspectes de producció real com el delay en obtenir etiquetes, patrons temporals reals (estacionalitat, tendències), o el volum continu de dades. En producció, cal infraestructura de streaming i monitoratge continu.
Amb quina freqüència actualitzar?
No hi ha una resposta universal. Depèn de:
Velocitat del canvi en les dades
Si les dades canvien ràpidament (per exemple, tendències en xarxes socials), necessitem actualitzacions freqüents. Si són estables (per exemple, detecció de defectes en manufactura), podem actualitzar menys sovint.
Cost de l’actualització
Cada actualització té costos:
- Computacional: Temps de GPU/CPU per entrenar
- Humà: Temps d’enginyers per validar i desplegar
- Risc: Possibilitat d’introduir regressions
Valor de la frescor (freshness)
No sempre tenir el model més recent és millor. Cal preguntar-se: quant guanyem amb dades més recents?
Un estudi de Facebook va mostrar que passar d’entrenament setmanal a diari reduïa la pèrdua del model en un 1%. Per a alguns casos, aquest 1% pot valer milions; per a altres, no justifica l’esforç.
Estratègies comunes
| Estratègia | Freqüència | Casos d’ús |
|---|---|---|
| Ad-hoc | Quan calgui | Models estables, canvis rars |
| Programada | Setmanal/mensual | Canvis graduals previsibles |
| Basada en triggers | Quan es detecta drift | Canvis imprevisibles |
| Contínua | Diària o més | Canvis molt ràpids |
Paradigmes d’actualització: Batch, Streaming, i Continual Learning
El terme “aprenentatge continu” (continual learning) pot ser ambigu. Aquí definirem clarament tres paradigmes diferents.
Què és Continual Learning?
Continual Learning no es refereix només a algoritmes d’aprenentatge incremental, sinó a crear infraestructura que permeti actualitzar models ràpidament quan calgui — des de zero o amb fine-tuning — i desplegar-los de manera àgil.
És una qüestió d’infraestructura i procés, no només d’algoritme.
Tres paradigmes d’actualització
1. Batch Retraining (Aprenentatge per lots)
- Model entrenat des de zero a intervals discrets
- Re-entrenament programat (setmanal, mensual)
- Pipeline de desplegament lent (dies a setmanes)
- Exemple: Re-entrenament mensual amb aprovació manual
2. Streaming Retraining (Re-entrenament amb finestres)
- Model re-entrenat des de zero sobre finestres de dades recents
- Finestra lliscant o expansiva d’exemples recents
- Pipeline més ràpid (hores a dies)
- Exemple: Re-entrenament diari amb els últims 30 dies de dades
3. Continual Learning (Infraestructura per actualitzacions ràpides)
- Infraestructura que suporta actualitzacions ràpides quan calgui
- Pot usar re-entrenament stateless (des de zero) O stateful (fine-tuning)
- Pipeline de desplegament ràpid (minuts a hores)
- Monitoratge, validació, i desplegament automatitzats
- Exemple: Re-entrenament automàtic quan es detecta drift, amb A/B testing i rollback automàtic
Taula comparativa
| Aspecte | Batch | Streaming | Continual Learning |
|---|---|---|---|
| Infraestructura | Manual/programada | Semi-automatitzada | Totalment automatitzada |
| Velocitat desplegament | Dies a setmanes | Hores a dies | Minuts a hores |
| Trigger | Programat o manual | Programat (freqüent) | Automàtic (drift, performance) |
| Validació | Revisió manual | Automatitzada amb llindars | Automatitzada + A/B testing |
| Monitoratge | Dashboards periòdics | Mètriques contínues | Alertes en temps real + auto-acció |
| Rollback | Manual | Semi-automàtic | Automàtic (instantani) |
| Millor per | Dades estables | Velocitat moderada | Entorns molt canviants |
Quan usar cada paradigma?
Batch Retraining:
- Canvis en dades lents i predictibles
- Revisió manual requerida (mèdic, financer)
- Equip petit, infraestructura simple
- Freqüència de setmanes/mesos acceptable
Streaming Retraining:
- Dades arriben contínuament, patrons canvien moderadament
- Equilibri entre frescor i validació
- Es poden definir finestres temporals significatives
Continual Learning:
- Patrons canvien ràpidament i impredictiblement (frau, spam)
- Necessitat de respondre a drift en hores
- Recursos d’enginyeria per pipelines automatitzats
- Flexibilitat: re-entrenament des de zero O fine-tuning
Desafiaments de Continual Learning
Challenge: Validació automatitzada
- Solució: Test suites complets, holdout sets, A/B testing
Challenge: Detecció de drift i triggering
- Solució: Monitoritzar distribucions, performance, confiança; activar per llindars
Challenge: Pipeline de desplegament ràpid
- Solució: Contenidors, CI/CD, testing automatitzat
Challenge: Rollback i versionat
- Solució: Model registry, blue-green deployment, rollback automàtic
Evitar sobre-enginyeria: La majoria de sistemes usen batch o streaming. Només implementar continual learning quan:
- Negoci requereix actualitzacions ràpides (hores, no dies)
- Patrons canvien ràpidament
- Tens recursos d’enginyeria per mantenir pipelines automatitzats
Casos d’ús i estratègies de re-entrenament
Un cop hem vist els paradigmes d’actualització, vegem com s’apliquen en contextos reals. La taula següent mostra diferents casos d’ús amb les seves característiques i l’estratègia de re-entrenament més adequada.
Ground truth (veritat de referència): el resultat real i verificat que ens indica si la predicció del model era correcta. Per exemple, en detecció de frau, el ground truth és la confirmació posterior de si la transacció era realment fraudulenta. La manera com obtenim aquest ground truth (automàticament, manualment, o amb quin delay) determina quina estratègia de re-entrenament podem seguir.
| Cas d’ús | Ground Truth | Delay | Paradigma | Freqüència |
|---|---|---|---|---|
| Recomanador e-commerce | Automàtic (clicks/compres) | Hores/dies | Streaming | Diari |
| Diagnòstic mèdic | Manual (experts) | Setmanes | Batch | Mensual |
| Detecció de frau bancari | Automàtic (verificació) | Dies | Continual Learning | Quan drift |
| Filtre de spam | Automàtic (feedback usuaris) | Immediat | Continual Learning | Continu |
| Predicció ocupació transport | Automàtic (sensors) | Minuts | Streaming | Diari |
| Classificació documents admin. | Manual (revisió) | Setmanes | Batch | Mensual |
| Detecció defectes manufactura | Automàtic (sensors) | Minuts/hores | Continual Learning | Temps real |
| Moderació xarxes socials | Mixt (reports + signals) | Hores/dies | Streaming | Diari |
| Predicció demanda energia | Automàtic (comptadors) | Hores | Streaming | Diari |
| Sistema recomanació biblioteca | Automàtic (préstecs) | Dies | Streaming | Setmanal |
Patrons observables
Analitzant aquests casos, veiem diversos patrons que ens ajuden a decidir l’estratègia:
Ground truth automàtic + delay curt → Continual Learning o Streaming
- Exemples: Spam filtering, detecció de frau, predicció de transport
- Permet actualitzacions freqüents i automatitzades
- Avantatge: model sempre actualitzat amb els patrons més recents
Ground truth manual → Batch
- Exemples: Diagnòstic mèdic, classificació de documents
- Requereix revisió humana, actualitzacions menys freqüents
- Avantatge: alta qualitat de les etiquetes, validació experta
Ground truth mixt → Streaming
- Exemples: Moderació de contingut, sistemes de recomanació
- Equilibri entre automatització i qualitat
- Avantatge: combina volum (automàtic) amb precisió (manual)
Crític per seguretat/salut → Batch amb validació humana
- Exemples: Diagnòstic mèdic, detecció de defectes crítics
- Encara que hi hagi drift, la revisió manual és imprescindible
- Avantatge: control de qualitat abans de desplegament
Consell pràctic: Comença sempre amb Batch i evoluciona cap a Streaming o Continual Learning només si:
- Tens prou dades noves cada dia/setmana
- Pots automatitzar la validació de manera fiable
- El valor de negoci justifica la complexitat addicional
De ground truth a dataset d’entrenament
Hem vist quins tipus de ground truth existeixen i amb quin delay arriben. Però com passem d’obtenir ground truth a tenir un dataset llest per re-entrenar? Aquest pas sovint es dona per fet, però requereix un disseny explícit.
Pas 1: Guardar prediccions amb context
Cada vegada que el model fa una predicció en producció, cal guardar-la juntament amb la informació necessària per vincular-la amb el ground truth futur:
| Camp | Descripció | Exemple |
|---|---|---|
prediction_id | Identificador únic | pred_2024_001 |
timestamp | Moment de la predicció | 2024-03-15 10:23:00 |
features | Inputs del model | {edat: 35, import: 500} |
prediction | Sortida del model | fraud: 0.87 |
model_version | Versió del model | v1.2.0 |
Sense aquest registre, quan arribi el ground truth no podrem associar-lo amb la predicció original.
Pas 2: Vincular ground truth amb prediccions
Quan el ground truth arriba (hores, dies, o setmanes després), cal fer el join amb les prediccions guardades:
Ground truth automàtic (clicks, sensors, verificacions):
- Un procés periòdic consulta la font de ground truth (base de dades de transaccions verificades, logs de clicks, lectures de sensors)
- Fa join per
prediction_ido per clau de negoci (ex:transaction_id) - Marca les prediccions com a “etiquetades”
Ground truth manual (experts, revisors):
- Les prediccions pendents d’etiquetar es presenten als revisors (interfície d’anotació, cua de revisió)
- Els revisors assignen l’etiqueta correcta
- Es fa join amb la predicció original
Ground truth que mai arriba:
- No totes les prediccions rebran ground truth (l’usuari no fa click, el revisor no arriba a revisar-ho)
- Cal decidir: descartar-les? Assumir que la predicció era correcta? Usar-les només per monitoratge de distribucions?
- En general, no incloure al dataset d’entrenament registres sense ground truth confirmat
Pas 3: Decidir la finestra de dades
Un cop tenim prediccions etiquetades, cal decidir quines incloure al dataset de re-entrenament:
Finestra expansiva (expanding window):
- Totes les dades acumulades des del principi:
training original + batch 1 + batch 2 + ... - Avantatge: més dades, més representativitat històrica
- Desavantatge: el dataset creix indefinidament; patrons antics poden diluir els recents
Finestra lliscant (sliding window):
- Només les dades dels últims N dies/setmanes:
últims 90 dies - Avantatge: el model s’adapta als patrons recents
- Desavantatge: perd context històric; pot fallar en patrons estacionals
Híbrid (comú en producció):
- Mostra representativa de dades històriques + totes les dades recents
- Exemple: 20% mostreig aleatori de dades antigues + 100% dels últims 30 dies
- Equilibri entre memòria històrica i adaptació a canvis recents
Pas 4: Validar el dataset resultant
Abans de re-entrenar, verificar que el dataset és coherent:
- Volum mínim: Suficients mostres per classe (evitar classes amb 2-3 exemples)
- Distribució d’etiquetes: No ha canviat dràsticament sense explicació (possible error en el procés de join)
- Completesa de features: No hi ha features amb molts nuls nous
- Temporalitat: Les dates de ground truth són posteriors a les prediccions (no hi ha fuites temporals)
Resum del procés
Producció Ground Truth Dataset
───────── ──────────── ───────
Predicció ──────► Guardar amb context
│
(passa el temps)
│
Arribar GT ──────► Join per ID
│
Triar finestra
│
Validar qualitat
│
Dataset per re-entrenar
Aquest procés és el que connecta la taula de casos d’ús anterior (quin ground truth tenim i amb quin delay) amb les decisions d’entrenament que veurem a continuació: entrenar des de zero o incrementalment, i com automatitzar-ho.
Entrenament amb estat vs. sense estat
Quan re-entrenem, tenim dues opcions principals:
Entrenament sense estat (Stateless training)
Entrenem el model des de zero amb totes les dades disponibles (històriques + noves).
Avantatges:
- Simple conceptualment
- El model veu totes les dades
- No arrossega errors anteriors
Desavantatges:
- Costós computacionalment si hi ha moltes dades
- Pot “oblidar” patrons antics si les dades recents dominen
Entrenament amb estat (Stateful training)
Continuem entrenant el model existent amb només les dades noves.
Avantatges:
- Molt més ràpid
- Menys recursos computacionals
- Ideal per a actualitzacions freqüents
Desavantatges:
- Risc d’oblit catastròfic (catastrophic forgetting): el model oblida el que sabia
- Pot acumular errors
Exemple real: Grubhub va passar d’entrenament setmanal sense estat a entrenament diari amb estat, reduint el cost computacional 45 vegades i augmentant les conversions un 20%.
Quin triar?
Una estratègia comuna és combinar-los:
- Entrenament amb estat per a actualitzacions freqüents (diari/setmanal)
- Entrenament sense estat periòdicament (mensual/trimestral) per “netejar” el model
Automatització del pipeline d’actualització
Per fer l’actualització sostenible, necessitem automatitzar-la. Aquí presentem un esquema basat en scripts Python simples.
Components del pipeline
Estructura d’un script d’actualització
Un script d’actualització automàtica (update_model.py) segueix aquest flux:
- Carregar dades noves des de la font configurada (el dataset construït al pas anterior)
- Validar dades (nuls, mínim de mostres, schema — veure també Pas 4 de la secció anterior)
- Entrenar model candidat amb les dades
- Avaluar i calcular mètriques (F1, accuracy)
- Comparar amb model actual (si existeix)
- Decidir: Si el candidat és millor o igual → guardar i desplegar; si no → rebutjar
- Registrar mètriques i metadata
L’script retorna exit code 0 (èxit) o 1 (error/rebutjat), permetent integració amb CI/CD i cron.
Programació amb cron
Per executar l’script automàticament, podem usar cron:
# Executar cada dia a les 3:00 AM
0 3 * * * /usr/bin/python3 /scripts/update_model.py >> /logs/cron.log 2>&1
Comparació de models abans de desplegar
Un dels passos més importants del pipeline d’actualització és comparar el model candidat amb el model actual abans de desplegar. Sense aquesta comparació, podem desplegar models pitjors accidentalment.
Per què comparar?
Problema sense comparació:
- Entrenem un model nou → sembla “bo” (F1 = 0.78)
- El despleguem → les mètriques empitjoren
- Resulta que el model anterior era millor (F1 = 0.82)!
Solució: Sempre comparar candidat vs actual.
Què comparar?
Mètriques tècniques:
- F1, precision, recall, accuracy
- ROC-AUC, PR-AUC
- Per regressió: MAE, RMSE, R²
Mètriques operacionals:
- Latència de predicció (p50, p95, p99)
- Mida del model (MB)
- Temps d’inferència
Mètriques de negoci (si disponibles):
- Cost estimat de prediccions errònies
- ROI esperat
- Impacte en conversió/vendes
Criteris de decisió
Definir regles clares per decidir si desplegar el candidat. Criteris típics:
| Criteri | Llindar típic | Acció si es viola |
|---|---|---|
| F1 degradat | > 2% | Rebutjar |
| Latència augmentada | > 50% | Rebutjar |
| Mida model | > +100MB | Rebutjar |
| F1 millorat | > 0% | Desplegar |
La lògica és: si el model candidat és igual o millor en mètriques i no empitjora significativament en aspectes operacionals → desplegar. Si no → mantenir l’actual.
Resum de la comparació
Essencial per continual learning:
- ✅ Sempre comparar candidat vs actual
- ✅ Usar múltiples dimensions (mètriques + latència + mida)
- ✅ Definir criteris clars de desplegament
- ✅ Fer backup abans de reemplaçar
Flux recomanat:
- Entrenar model candidat
- Comparar amb actual (mètriques, latència, mida)
- Decidir automàticament basant-se en regles
- Si aprovat → backup + desplegament
- Si rebutjat → descartar candidat
Amb comparació automàtica, evitem desplegar models pitjors i mantenim qualitat constant!
Test en producció
Un cop tenim un model candidat que sembla bo en les proves offline, com sabem si funcionarà bé en producció real? Aquí és on entren les estratègies de test en producció (testing in production).
El problema
Les proves offline (amb dades històriques) tenen limitacions:
- No capturen el comportament real dels usuaris
- No detecten problemes d’integració amb altres sistemes
- No mesuren l’impacte en mètriques de negoci reals
Per això, necessitem estratègies per provar models en producció de manera controlada.
Shadow Deployment (Desplegament ombra)
El nou model rep les mateixes peticions que el model actual, però les seves prediccions no s’utilitzen. Només serveixen per comparar.
Avantatges:
- Zero risc per als usuaris
- Podem comparar prediccions directament
- Detectem problemes d’integració
Desavantatges:
- Duplica el cost computacional
- No mesura l’impacte real en el comportament dels usuaris
Implementació: L’API carrega ambdós models. Per cada petició, executa els dos però només retorna la predicció del model actual. Les prediccions del shadow es registren per anàlisi posterior.
A/B Testing
Dividim el tràfic entre el model actual (A) i el nou model (B), i mesurem quina versió funciona millor segons mètriques de negoci.
Avantatges:
- Mesura l’impacte real en mètriques de negoci
- Estadísticament rigorós si es fa bé
Desavantatges:
- Part dels usuaris reben el model potencialment pitjor
- Requereix més temps per tenir resultats significatius
- Necessita infraestructura per dividir tràfic
Implementació: Usar un hash del user_id per assignar consistentment cada usuari a la versió A o B (així el mateix usuari sempre rep la mateixa versió). Registrar la versió amb cada predicció per poder analitzar mètriques per grup.
Canary Deployment (Desplegament canari)
Similar a l’A/B testing, però amb l’objectiu de detectar problemes, no de comparar rendiment. Enviem un petit percentatge del tràfic al nou model i augmentem gradualment si tot va bé.
Avantatges:
- Limita l’impacte de problemes
- Permet rollback ràpid
- Detecta problemes que no apareixen en tests
Desavantatges:
- Requereix monitoratge proper durant el desplegament
- Més lent que un desplegament directe
Bandits (Multi-armed bandits)
Els bandits són una alternativa més sofisticada a l’A/B testing. En lloc de dividir el tràfic de manera fixa, el sistema aprèn dinàmicament quina versió funciona millor i li envia més tràfic.
Inici: Model A: 50% | Model B: 50%
Dia 3: Model A: 60% | Model B: 40% (A sembla millor)
Dia 7: Model A: 80% | Model B: 20% (A confirmat millor)
Avantatges:
- Minimitza el “regret” (tràfic enviat al model pitjor)
- S’adapta automàticament
Desavantatges:
- Més complex d’implementar
- Menys control sobre la divisió del tràfic
- Pot ser prematur si les diferències són petites
Per al nostre context, l’A/B testing o el canary deployment són més pràctics i fàcils d’entendre i implementar.
Comparativa de les estratègies
| Estratègia | Risc | Complexitat | Mesura impacte real |
|---|---|---|---|
| Shadow | Cap | Baixa | No |
| A/B Test | Moderat | Mitjana | Sí |
| Canary | Baix | Mitjana | Parcialment |
| Bandits | Variable | Alta | Sí |
Recomanació pràctica
Un flux recomanat per a equips petits:
- Shadow deployment primer: Verificar que el model nou funciona sense errors
- Canary amb 5-10%: Detectar problemes d’integració
- Si tot va bé, desplegament complet o A/B test si volem mesurar impacte
Consideracions pràctiques
Versionat de models
El versionat de models (semantic versioning, metadata, estructura de directoris, symlinks) es tracta en detall al capítol de desplegament. Aquí ens centrem en els aspectes específics del continual learning.
Per què és més crític en continual learning?
Quan els models canvien freqüentment (setmanal, diari, o més), el versionat passa de ser una bona pràctica a ser imprescindible:
- Cal comparar nou model vs actual abans de desplegar
- Si un model nou falla, necessitem rollback immediat
- Hem de poder auditar quin model va fer quina predicció i quan
Metadata extesa per continual learning
A més de la metadata bàsica (versió, mètriques, features), el continual learning requereix:
- Dataset info: Quantes dades, de quin període, hash del dataset
- Data version: Quin split de dades es va usar per entrenar
- Training time: Quan es va entrenar i quant temps va trigar
Aquesta informació és essencial per reproduir entrenaments i diagnosticar regressions.
Comparació automàtica abans de desplegar
Abans d’actualitzar el symlink current -> v1.3.0, cal comparar les mètriques del candidat vs. l’actual. Si el candidat és igual o millor → desplegar; si no → mantenir l’actual. Aquesta comparació s’hauria d’automatitzar dins del pipeline d’actualització.
Històric de desplegaments
Mantenir un fitxer deployment_history.json que registri cada desplegament: versió, data, raó del canvi, i quina versió l’ha reemplaçat. Això permet auditar l’historial i entendre per què s’han fet canvis.
Rollback
Sempre hem de poder tornar a la versió anterior ràpidament:
#!/bin/bash
# rollback.sh - Torna a la versió anterior del model
set -e # Sortir si hi ha errors
CURRENT=$(readlink -f /models/current_model.pkl || echo "")
if [ -z "$CURRENT" ]; then
echo "Error: No s'ha trobat el model actual"
exit 1
fi
PREVIOUS=$(find /models -name 'model_v*.pkl' -type f | grep -v "$CURRENT" | sort -r | head -1)
if [ -z "$PREVIOUS" ]; then
echo "Error: No hi ha model anterior disponible"
exit 1
fi
echo "Rollback de $CURRENT a $PREVIOUS"
ln -sf "$PREVIOUS" /models/current_model.pkl
# Reiniciar el contenidor per carregar el model antic
if docker restart model_api; then
echo "Rollback completat amb èxit"
else
echo "Error: Fallida al reiniciar el contenidor"
exit 1
fi
Documentació de canvis
Mantenir un registre de tots els canvis:
# Changelog de models
## v1.1.0 (2024-02-01)
- Afegides 3 noves features
- Re-entrenat amb dades de gener
- Accuracy: 0.89 (+0.02)
## v1.0.0 (2024-01-15)
- Versió inicial
- Accuracy: 0.87
Resum
En aquest capítol hem après:
- L’aprenentatge continu és necessari perquè els models s’adaptin als canvis en les dades
- La freqüència d’actualització depèn de la velocitat del canvi, el cost, i el valor de la frescor
- El pas de ground truth a dataset d’entrenament requereix guardar prediccions amb context, vincular-les amb el resultat real quan arribi, i decidir la finestra de dades adequada
- L’entrenament amb estat és més eficient però pot acumular errors; combinar-lo amb entrenament sense estat periòdicament és una bona estratègia
- Un pipeline automatitzat amb scripts Python pot gestionar la recollida de dades, entrenament, validació, i desplegament
- Les estratègies de test en producció (shadow, A/B testing, canary) permeten validar models amb tràfic real de manera controlada
- El versionat i la capacitat de rollback són essencials per a operacions segures
Amb això completem la visió del cicle de vida d’un model en producció: des del desplegament inicial amb Docker i FastAPI, passant pel monitoratge i detecció de drift, fins a l’actualització contínua i el test en producció.
Patrons d’implementació per MLOps
- Com usar aquest document
- Part 1: Estructura i tooling
- Part 2: Gestió de configuració i dades
- Part 3: Servir prediccions
- Part 4: Persistència i base de dades
- Part 5: Testing
- Part 6: Qualitat i operacions
- Resum: Preguntes per cada patró
Com usar aquest document
Aquest no és un manual per copiar i enganxar. Cada patró resol un problema concret que trobareu quan porteu un model de ML a producció. Abans de mirar el codi, enteneu:
- Quin problema resol - Per què necessiteu aquest patró?
- Quan aplicar-lo - En quines situacions és útil?
- Quines decisions heu de prendre - Què heu de pensar abans d’implementar?
Quan trobeu un problema al vostre projecte, busqueu el patró corresponent aquí.
Part 1: Estructura i tooling
Patrons per organitzar el projecte. Aquestes són les bases que cal establir abans de construir funcionalitats.
Patró: Module-as-Script (python -m)
El problema
python scripts/train.py # Funciona?
./scripts/train.py # I això?
cd scripts && python train.py # I des d'aquí?
Depèn del directori, del PYTHONPATH, de moltes coses.
La solució
python -m app.train # Sempre funciona
python -m app.pipeline # Des de qualsevol lloc
Com implementar-ho
# app/train.py
def main():
# Lògica principal
if __name__ == "__main__":
main()
Ara podeu:
- Executar:
python -m app.train - Importar:
from app.train import main - Testejar:
main()directament
Patró: Path Resolution
El problema
model = joblib.load("models/model.pkl") # Depèn del working directory
model = joblib.load("/home/joan/model.pkl") # No portable
La solució
# config.py
from pathlib import Path
PROJECT_ROOT = Path(__file__).parent.parent
class Settings(BaseSettings):
model_path: str = "models/model.pkl"
def get_model_path(self) -> Path:
return PROJECT_ROOT / self.model_path
Ara funciona des de qualsevol directori i a Docker.
Patró: Makefile com a Interfície
El problema
- Cada desenvolupador executa comandes diferents
- Difícil recordar tots els flags
- CI/CD duplica scripts
La solució
El Makefile és el vostre contracte d’interfície:
setup:
python -m venv venv && pip install -r requirements.txt
test:
pytest tests/ -v
docker-up:
docker compose up -d
health:
curl http://localhost:8000/health
Tothom interactua amb el projecte igual: make test, make docker-up, make health.
Avantatge clau
Si make health funciona localment, funcionarà al servidor CI (mateixes comandes).
Patró: Structured Logging
El problema
Quan alguna cosa falla en producció, com sabeu què ha passat? La primera instíncta és usar print(), però té limitacions clares:
print() | import logging | |
|---|---|---|
| Format | Cada llamada té el seu propi format | Un seol format definit una vegada, aplicat a tota la sortida |
| Sortides | Sempre sols stdout | Podeu enviar a consola, fitxer, servei extern… sense canviar el codi |
| Filtrat | No podeu desactivar-lo sense esborrar línies | Controleu el nivell (DEBUG, INFO, ERROR…) per entorn |
| Ubicació | Cal cercar on va fer el print | Cada missatge porta automàticament el nom del module i la línia |
La clau és que amb logging configureu una vegada i tot el codi del projecte n’aprofita automàticament.
Nivells de log
| Nivell | Quan usar-lo |
|---|---|
| DEBUG | Detalls per desenvolupament |
| INFO | Confirmació que les coses funcionen |
| WARNING | Alguna cosa inesperada però no crítica |
| ERROR | Error que impedeix una operació |
| CRITICAL | Error greu, el sistema pot fallar |
Estructura bàsica
Primer, configureu el root logger amb els dos handlers — un per consola i un per fitxer. Feu-ho una vegada, normalment al punt d’entrada del projecte (ex: main() o app/config.py):
import logging
def setup_logging(log_level: str = "INFO", log_file: str = "app.log"):
handlers = [
logging.StreamHandler(), # Consola (stdout)
logging.FileHandler(log_file), # Fitxer
]
logging.basicConfig(
level=getattr(logging, log_level.upper()),
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
handlers=handlers,
)
Després, a cada module on necessiteu logar, creeu un logger amb el nom del module:
import logging
logger = logging.getLogger(__name__)
logger.info("Model carregat: version=%s", version)
logger.error("Predicció fallida", exc_info=True) # Inclou traceback
Penseu-hi
- Què logar: Startup, cada predicció (sense dades sensibles), errors
- On enviar: Console (per Docker), fitxer (per persistència)
- Quin nivell per entorn: DEBUG en dev, INFO/WARNING en prod
Error típic
“Uso
print()per debugging”
Useu logger.debug(). En producció podeu desactivar debug sense tocar el codi.
Part 2: Gestió de configuració i dades
Un projecte real té configuració (paths, ports, credencials) i dades que cal processar. Com organitzar-ho?
Patró: Configuration Management
El problema
Mireu aquest codi:
model = joblib.load("/home/joan/projecte/models/model.pkl")
DB_PASSWORD = "secret123"
Què passa quan:
- Un altre membre de l’equip clona el projecte?
- Voleu executar en un servidor amb paths diferents?
- Algú veu la contrasenya al repositori git?
La solució: Variables d’entorn
Separeu què (el codi) de on/com (la configuració):
.env (fitxer) → pydantic-settings (validació) → settings (objecte Python)
Penseu-hi
Abans d’implementar, classifiqueu la vostra configuració:
| Configuració | Obligatòria? | Té default? | És secreta? |
|---|---|---|---|
| MODEL_PATH | Sí | No | No |
| API_PORT | No | Sí (8000) | No |
| DB_PASSWORD | Sí | No | Sí |
Les obligatòries sense default fallaran si no existeixen - això és bo, detecteu errors ràpid.
Estructura bàsica
# config.py
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
model_path: str # Obligatòria
api_port: int = 8000 # Amb default
log_level: str = "INFO" # Amb default
class Config:
env_file = ".env"
settings = Settings()
Ara a qualsevol lloc:
from config import settings
model = joblib.load(settings.model_path)
Error típic
“Tinc
config.pyperò també faigos.environ.get()en altres llocs”
Centralitzeu tota la configuració. Un sol punt d’entrada.
Patró: ETL per ML
El problema
Les dades rarament arriben netes i llestes. Necessiteu:
- Obtenir-les (d’un fitxer, base de dades, API…)
- Transformar-les (validar, netejar, crear features…)
- Guardar-les (en format adequat per entrenar)
Això és ETL: Extract, Transform, Load.
Penseu-hi
Per cada fase:
Extract:
- D’on venen les dades?
- Són massa grans per carregar a memòria d’un cop?
Transform:
- Quines validacions necessiteu?
- Què feu amb registres invàlids?
- Quines transformacions apliqueu?
Load:
- Quin format de sortida? (CSV llegible, Parquet eficient)
- Guardeu metadades (quants registres, quan es va processar)?
Estructura bàsica
def run_pipeline(input_path: Path, output_dir: Path):
# EXTRACT
df = pd.read_csv(input_path)
# TRANSFORM
df = validar(df)
df = netejar(df)
df = crear_features(df)
# SPLIT (específic per ML)
train, val, test = dividir(df)
# LOAD
guardar(train, val, test, output_dir)
Principis importants
- Idempotència: Executar dues vegades → mateix resultat
- Logging: Registrar cada pas (quants registres entren/surten)
- Error handling: Un registre erroni no ha de fer fallar tot
Error típic
“Faig les transformacions al notebook i copio les dades manualment”
El pipeline ha de ser un script executable. Si canvia el dataset, només cal tornar a executar.
Part 3: Servir prediccions
Quan el model està entrenat, heu de decidir com el fareu accessible. Aquesta decisió té impacte en tota l’arquitectura.
Patró: Online vs Batch Prediction
El problema
Teniu un model entrenat. Ara què? Hi ha dues estratègies fonamentalment diferents:
| Online | Batch | |
|---|---|---|
| Metàfora | Un cambrer que serveix plats a demanda | Una cuina industrial que prepara 1000 menús |
| Quan s’executa | Quan arriba cada petició | Programat (cada hora, nit, setmana…) |
| Qui espera | L’usuari, davant la pantalla | Ningú - és un procés en background |
Penseu-hi
Abans d’implementar, responeu aquestes preguntes:
- L’usuari final espera una resposta immediata?
- Necessiteu processar moltes dades d’un cop (milers de registres)?
- La predicció és per a una acció immediata o per a un report?
Si l’usuari espera → Online. Si processeu en background → Batch.
Quan usar Online
- Recomanacions mentre l’usuari navega
- Detecció de frau en una transacció
- Predicció de preu en una app
L’estructura bàsica és:
Petició HTTP → Validar input → Model.predict() → Retornar JSON
Quan usar Batch
- Generar un report diari amb prediccions per tots els clients
- Preprocessar un dataset gran per anàlisi
- Calcular scores que es consultaran després (cache)
L’estructura bàsica és:
Llegir fitxer → Iterar per chunks → Guardar resultats
Error típic
“Faig un bucle que crida l’API per cada registre del CSV”
Això és el pitjor dels dos mons: la latència d’online amb el volum de batch. Si teniu molts registres, processeu-los en batch o envieu-los tots de cop a l’API.
Patró: Error Handling
El problema
En producció, les coses fallen. La diferència entre un sistema amateur i un professional és com comunica els errors.
Compareu:
- Amateur: “500 Internal Server Error”
- Professional: “El camp ‘age’ ha de ser un número positiu (rebut: -5)”
Els codis HTTP que heu de conèixer
| Codi | Significat | Exemple |
|---|---|---|
| 200 | Tot bé | Predicció exitosa |
| 400 | Error de l’usuari | Dades invàlides |
| 422 | Dades mal formades | JSON incorrecte |
| 500 | Error del servidor | Bug al codi |
| 503 | Servei no disponible | Model no carregat |
Penseu-hi
- Quins errors poden passar? (Feu una llista)
- Quins són culpa de l’usuari? (→ 4xx)
- Quins són culpa del sistema? (→ 5xx)
- Quin missatge és útil per l’usuari sense revelar detalls interns?
Estructura recomanada
@app.post("/predict")
def predict(features: Features):
# 1. Validació de negoci
if not es_valid(features):
raise HTTPException(400, detail="Explicació clara del problema")
try:
# 2. Lògica principal
prediction = model.predict(...)
return {"prediction": prediction}
except HTTPException:
raise # Re-llençar errors HTTP
except Exception as e:
# 3. Log intern (amb detalls), resposta genèrica (sense detalls)
logger.error("Error", exc_info=True)
raise HTTPException(500, detail="Error intern del servidor")
La clau és: logs detallats per vosaltres, missatges clars per l’usuari.
Part 4: Persistència i base de dades
Ara que l’API serveix prediccions i gestiona errors, necessiteu guardar-les per auditoria, debugging i monitoring.
Patró: SQLAlchemy amb FastAPI
El problema
Voleu guardar cada predicció per:
- Auditoria: Qui va demanar què i quan
- Debugging: Investigar prediccions estranyes
- Monitoring: Estadístiques d’ús i latència
Penseu-hi
-
Quina base de dades?
- SQLite: Zero configuració, ideal per prototips
- PostgreSQL: Robust, per producció real
-
Què guardeu de cada predicció?
- Timestamp
- Input features (totes? les importants?)
- Output
- Versió del model
- Latència
Estructura bàsica
Primer, definiu què voleu guardar:
class PredictionLog(Base):
__tablename__ = "predictions"
id = Column(Integer, primary_key=True)
timestamp = Column(DateTime, default=datetime.utcnow)
# TODO: Decidiu quins camps d'input guardeu
# ...
prediction = Column(Float)
model_version = Column(String)
latency_ms = Column(Float)
Després, integreu amb l’API usant dependency injection.
Patró: Dependency Injection
El problema
L’endpoint de predicció necessita accés a:
- El model
- La base de dades
- La configuració
Com ho organitzeu sense crear un embolic?
Sense DI (problemàtic)
@app.post("/predict")
def predict(features: Features):
db = SessionLocal() # Crear connexió
model = load_model() # Carregar model
try:
# ... lògica ...
finally:
db.close() # Recordar tancar!
Problemes:
- Difícil de testejar (sempre usa recursos reals)
- Fàcil oblidar netejar recursos
- Lògica barrejada amb infraestructura
Amb DI (recomanat)
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
@app.post("/predict")
def predict(features: Features, db: Session = Depends(get_db)):
# FastAPI gestiona la connexió automàticament
# ...
Avantatge clau: Per testejar, podeu substituir get_db per una funció que retorna una BD de test.
Part 5: Testing
Com testegeu un sistema de ML?
Patró: Testing per ML Systems
El problema
Els sistemes ML tenen parts deterministes (codi) i parts estocàstiques (el model). No podeu testejar tot igual.
Penseu-hi
Què SÍ testejar:
- Validació d’entrada: Input vàlid passa, invàlid falla
- Format de sortida: La predicció té l’estructura esperada
- Endpoints: Retornen els codis HTTP correctes
- Preprocessament: Les transformacions funcionen
Què NO testejar:
- Accuracy exacta del model (varia entre entrenaments)
- Prediccions específiques (el model canvia)
Estructura bàsica amb pytest
# test_api.py
def test_predict_valid_input(client, sample_input):
response = client.post("/predict", json=sample_input)
assert response.status_code == 200
assert "prediction" in response.json()
def test_predict_invalid_input(client):
invalid = {"age": -5} # Edat negativa
response = client.post("/predict", json=invalid)
assert response.status_code == 400 # Error d'usuari
Fixtures per reutilitzar setup
# conftest.py
@pytest.fixture
def sample_valid_input():
return {"age": 30, "income": 50000}
@pytest.fixture
def test_client():
return TestClient(app)
Error típic
“El meu test verifica que
predict(X) == 0.847”
Els models no són deterministes. Testegeu que la predicció és un float entre 0 i 1, no un valor concret.
Patró: Test Database Fixtures
El problema
Tests que usen la BD real són:
- Lents
- Fràgils (depenen d’estat extern)
- Problemàtics en CI/CD
La solució: BD temporal en memòria
@pytest.fixture
def test_db():
# Crear BD en memòria
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(bind=engine)
session = sessionmaker(bind=engine)()
try:
yield session
finally:
session.close()
Cada test rep una BD buida i aïllada.
Part 6: Qualitat i operacions
El testing valida que el codi és correcte. Ara, abans de desplegar, necessiteu també validar que el sistema complet està llest per producció.
Patró: Quality Gates
El problema
Un model serialitzat existeix (.pkl), però pot tenir accuracy de 0.3, features incompatibles, o un training que va fallar silenciosament. Com eviteu que arribi a producció?
Penseu-hi
Definiu els vostres criteris de desplegament abans d’implementar:
- Quina mètrica és crítica pel vostre problema? (accuracy, F1, MAE…)
- Quin és el llindar mínim acceptable?
- Hi ha altres condicions? (features presents, latència màxima…)
Documenteu aquests criteris en un fitxer de configuració (config/deployment_criteria.yaml o similar).
Com implementar-ho
Principi clau: El model no es considera desplegable per defecte. Ha de passar una validació explícita. Només llavors es marca com deployment_ready: true a la metadata.
Al final del vostre script d’entrenament:
# Després d'entrenar i avaluar
metrics = {"accuracy": accuracy, "f1_score": f1}
# Carregar criteris
criteria = load_criteria("config/deployment_criteria.yaml")
# Decisió: passa o no passa?
deployment_ready = (
metrics["accuracy"] >= criteria["min_accuracy"] and
metrics["f1_score"] >= criteria["min_f1"]
)
# Guardar metadata amb el flag
metadata = {
"version": version,
"metrics": metrics,
"deployment_ready": deployment_ready # Resultat de la validació
}
save_metadata(metadata)
El hook pre-deploy.sh del CI/CD llegeix deployment_ready de la metadata. Si és False → el pipeline falla i el model no es desplega.
make test # Tests passen?
python validate.py # Model deployment_ready?
make docker-build # Es pot construir?
make health # Servei respon?
Si qualsevol passa falla → NO es desplega.
Error típic
“El meu training script sempre posa
deployment_ready: true”
Si el flag és sempre True independentment de les mètriques, no esteu protegint res. El valor ha de ser el resultat d’una validació real contra criteris definits prèviament.
Patró: Health Check
El problema
Com sabeu si el servei està funcionant correctament?
- Els load balancers necessiten saber si poden enviar tràfic
- El monitoring necessita alertar si hi ha problemes
- Després d’un deploy, voleu verificar que tot funciona
Penseu-hi
Què ha de verificar el health check?
- Model carregat? (
model is not None) - Base de dades accessible? (
db.execute("SELECT 1")) - Espai en disc suficient?
Estructura bàsica
@app.get("/health")
def health_check():
model_ok = model is not None
db_ok = check_database()
if model_ok and db_ok:
return {"status": "healthy", "model_version": VERSION}
else:
raise HTTPException(503, detail="Service unhealthy")
Error típic
“El health check fa una predicció real per verificar el model”
Els health checks han de ser ràpids (< 1 segon). Verificar que el model està carregat és suficient.
Patró: Model Versioning
El problema
Teniu un model en producció. Entreno un de nou. Com gestioneu les versions?
Penseu-hi
El versionat de models (semantic versioning, metadata, estructura de directoris, symlinks, model registry) es tracta en detall al capítol de desplegament. El punt clau per a implementació és afegir el flag deployment_ready a la metadata:
models/
├── model_v1.0.0.pkl # Model serialitzat
└── metadata_v1.0.0.json # Informació sobre el model
El metadata ha d’incloure: versió, data d’entrenament, mètriques, noms de features, i el flag deployment_ready (generat pel Quality Gate).
Patró: Rollback
El problema
Heu desplegat un model nou (v1.2.0). Les mètriques de producció mostren problemes: el temps de resposta és massa alt o les prediccions són pitjors del que esperàveu. Necessiteu tornar a la versió anterior (v1.1.0) ràpidament.
Penseu-hi
Què necessiteu tenir preparat abans que passi el problema?
- Les versions anteriors del model guardades
- Una manera de canviar quin model es carrega sense modificar codi
- Un procés de reinici ràpid
La solució
Si el model es carrega des d’una variable d’entorn:
# config.py
class Settings(BaseSettings):
model_path: str = "models/model_v1.2.0.pkl"
Fer rollback és:
# 1. Canviar .env
MODEL_PATH=models/model_v1.1.0.pkl
# 2. Reiniciar el servei
docker-compose restart api
Sense canviar codi, sense fer commit, sense CI/CD.
Què fa possible un rollback ràpid
- Versions guardades: Mai sobreescriu el model anterior
- Configuració externa: El path del model és una variable, no hardcoded
- Metadata: Saps quina versió estàs servint i pots comparar
Error típic
# ❌ Sempre carrega "model.pkl"
model = joblib.load("models/model.pkl")
Quan fas un nou entrenament, sobreescrius el model anterior. Si alguna cosa falla, no pots tornar enrere.
# ✓ Versions explícites
model = joblib.load(settings.model_path) # Controlat per .env
Resum: Preguntes per cada patró
Quan implementeu un patró, responeu primer:
| Patró | Pregunta clau |
|---|---|
| Module-as-Script | Com executar scripts de manera consistent? |
| Path Resolution | Com referenciar fitxers independentment del working directory? |
| Makefile | Quines operacions ha de poder fer qualsevol membre de l’equip? |
| Logging | Què necessiteu saber quan alguna cosa falla? |
| Configuration | Quines configuracions són obligatòries vs opcionals? |
| ETL | D’on venen les dades i com les valideu? |
| Online vs Batch | L’usuari espera resposta immediata? |
| Error Handling | Quins errors poden passar i qui és responsable? |
| SQLAlchemy | Què guardeu de cada predicció? |
| Dependency Injection | Quins recursos necessita cada endpoint? |
| Testing | Què és determinista (testejar) vs estocàstic (no testejar)? |
| Quality Gates | Quins criteris ha de passar el codi abans de desplegar? |
| Health Check | Quins components ha de verificar? |
| Model Versioning | Quan incrementeu major/minor/patch? |
| Rollback | Què necessiteu tenir preparat abans que el model falli? |
Referències
Diccionari d’Aprenentatge Automàtic
- Conceptes Bàsics i Paradigmes d’Aprenentatge
- Dades i variables
- Models i Algoritmes
- Entrenament, Avaluació i Optimització
- Qualitats dels models
- Arquitectures de Xarxes
- Mecanismes d’Aprenentatge
- Intel·ligència Artificial Generativa i Models de Llenguatge
- Àrees Aplicades i Tipus de Problemes
- Llista d’algoritmes
- Conceptes matemàtics
- Abreviatures
Conceptes Bàsics i Paradigmes d’Aprenentatge
Aprenentatge Supervisat (Supervised Learning):
Aprenentatge a partir d’exemples etiquetats on l’algoritme aprèn a mapar entrades amb sortides conegudes. Inclou classificació i regressió.
Aprenentatge No Supervisat (Unsupervised Learning):
Descoberta de patrons en dades sense etiquetes, com l’agrupament (clustering) i la reducció de dimensionalitat.
Aprenentatge per Reforç (Reinforcement Learning):
Agent que aprèn a prendre decisions interactuant amb un entorn, rebent recompenses o penalitzacions.
Aprenentatge Semi-supervisat i Auto (Semi-supervised Learning):
Combina una petita quantitat de dades etiquetades amb moltes dades no etiquetades per millorar el rendiment.
Aprenentatge Auto-supervisat (Self-supervised Learning):
Utilitza dades no etiquetades generant automàticament senyals d’entrenament (pseudo-etiquetes) per aprendre sense supervisió explícita.
Dades i variables
Conjunt de Dades (Dataset):
Col·lecció estructurada d’exemples utilitzada per entrenar, validar i avaluar models de machine learning. Pot dividir-se en conjunts d’entrenament, validació i test.
Exemple (Example/Instance):
Una única observació o mostra del conjunt de dades que conté les característiques d’entrada (x) i, en aprenentatge supervisat, la seva etiqueta o valor objectiu corresponent (y). Representa una unitat completa d’informació que el model utilitza per aprendre o fer prediccions.
Característica / atribut / x (feature):
Variables d’entrada que els models utilitzen per predir. Representen les propietats mesurables o observables dels exemples que ajuden el model a aprendre patrons i fer prediccions sobre la variable objectiu. En notació matemàtica es representa com “x”.
Objectiu / resposta / y (target): Valor correcte associat a cada exemple. En regressió, correspon a un valor numèric continu (ex. temperatura, preu). En classificació, s’anomena etiqueta o classe (label) i representa una categoria discreta (ex. “gat”, “gos”, “spam”). En notació matemàtica, s’acostuma a representar com (y), i es compara amb la predicció del model (ŷ) per calcular l’error.
Predicció / estimació / ŷ (prediction): Els valors que genera el model com a sortida després del processament de les dades d’entrada. Són les estimacions o prediccions que fa el model, que es comparen amb els valors reals per avaluar el rendiment. En notació matemàtica es representa com “ŷ” (y amb accent circumflex).
Dades Esbiaixades (Biased Data):
Quan el conjunt de dades no representa adequadament la diversitat real, portant a models discriminadors o poc generalitzables.
Dades Desbalancejades (Imbalanced Data):
Situació on algunes classes tenen molt menys exemples que altres, afectant el rendiment. Poden requerir tècniques específiques (com sobremostreig o penalització).
Conjunt d’Entrenament (Training Set):
Subconjunt de dades utilitzat per ajustar els paràmetres del model durant la fase d’entrenament. Aquestes dades són les que el model “veu” i aprèn a reproduir o predir.
Conjunt de Validació (Validation Set):
Subconjunt de dades (opcional) no utilitzat durant l’entrenament que serveix per avaluar la capacitat de generalització del model i per ajustar hiperparàmetres, evitant així sobreajustament.
Conjunt de Prova (Test Set):
Subconjunt de dades completament independent que s’utilitza per mesurar de manera objectiva el rendiment final del model després de l’entrenament i la validació. Representa la seva capacitat en dades noves i desconegudes.
Outliers (Valors Atípics):
Observacions que es desvien notablement de la majoria de les dades. Poden ser causats per errors de mesura, soroll o fenòmens inusuals, i poden afectar negativament el rendiment dels models.
Models i Algoritmes
Conceptes bàsics
Algoritme (Algorithm):
Conjunt d’instruccions ben definides que permeten resoldre un problema o executar una tasca. És un concepte general en informàtica. En aprenentatge automàtic, un algoritme especifica com aprendre patrons a partir de dades. No conté informació específica sobre cap conjunt de dades fins que s’entrena. En aplicar-lo a dades d’entrenament, es genera un model concret.
Model (Model):
Representació concreta obtinguda en aplicar un algoritme a dades d’entrenament. Conté els paràmetres apresos que defineixen com el model fa prediccions o classificacions sobre noves dades. És la instància que s’utilitza després de l’entrenament.
Arquitectura (del model): Estructura interna que defineix com s’organitzen i connecten els components d’un model, especialment en xarxes neuronals. Determina la forma i la complexitat del model, incloent el nombre de capes, el tipus d’unitats (neurones), com flueix la informació entre capes i quines funcions d’activació s’utilitzen. És un disseny reutilitzable amb diferents conjunts de dades i configuracions.
Paràmetres (Parameters):
Valors interns del model que s’ajusten durant l’entrenament per capturar la relació entre les característiques d’entrada i la variable objectiu. Exemples comuns són els pesos i el biaix. Són específics per cada model.
Pesos (Weights):
Paràmetres que modulen la influència de cada característica d’entrada sobre la predicció final. Normalment representen l’importància relativa de cada variable.
Biaix (Bias):
Paràmetre que permet ajustar la sortida del model independentment de les entrades, facilitant que el model pugui fer prediccions més flexibles i ajustades.
Tipus de models segons el comportament o objectiu
Model Determinista (Deterministic Model):
Un model que, donada una mateixa entrada, sempre genera la mateixa sortida exacta, sense incertesa ni variabilitat. Exemple: una funció matemàtica pura com la regressió lineal sense soroll addicional.
Model Estocàstic (Stochastic Model):
Model que incorpora components aleatoris en el seu funcionament o entrenament. Pot produir diferents resultats amb la mateixa entrada degut a l’atzar. No implica necessàriament càlculs explícits de probabilitat. A diferència dels models probabilístics, no sempre fan servir explícitament distribucions de probabilitat.
Model Probabilístic (Probabilistic Model):
Model que representa explícitament la incertesa mitjançant distribucions de probabilitat. Utilitza la teoria de la probabilitat per fer inferències i prediccions. És un cas particular de model estocàstic, on la incertesa es representa explícitament mitjançant distribucions de probabilitat.
Model Generatiu:
Aprèn la distribució conjunta de les dades d’entrada i les sortides. Pot generar noves dades similars a les d’entrenament.
Model Discriminatiu:
Aprèn la frontera de decisió entre classes o la relació entre entrada i sortida. S’utilitza per fer classificacions o prediccions directes.
Mètodes d’Ensemble (Ensemble Methods):
Combinació de diversos models per millorar rendiment (bagging, boosting, stacking).
Entrenament, Avaluació i Optimització
Cicle de vida d’un model
Entrenament (Training):
Fase en què l’algoritme ajusta els paràmetres interns del model per minimitzar l’error sobre les dades d’entrenament, mitjançant una funció de pèrdua. Aquest procés es duu a terme amb tècniques d’optimització com el descens de gradient.
Validació (Validation):
Fase on es comprova com generalitza el model a dades no vistes durant l’entrenament. Es fa servir per ajustar hiperparàmetres, detectar sobreajustament i seleccionar el millor model abans de la seva avaluació final.
Avaluació (Evaluation):
Fase final del desenvolupament del model en què es mesura el rendiment del model ja entrenat i validat, utilitzant un conjunt de proves (test set) independent. Permet estimar com es comportarà el model en un entorn real.
Inferència (Inference):
Fase en què s’utilitza el model ja entrenat i validat per fer prediccions sobre dades noves i desconegudes. És el moment en què el model s’aplica en situacions reals per resoldre la tasca per a la qual va ser dissenyat, sense fer cap ajust addicional als seus paràmetres.
Conceptes tècnics clau
Hiperparàmetres (Hyperparameters):
Són valors configurables que determinen el comportament del procés d’entrenament abans que comenci, i no s’aprenen automàticament a partir de les dades. Inclouen, per exemple, la taxa d’aprenentatge, el nombre de capes d’una xarxa neuronal, o la mida dels batch.
Funció de Pèrdua (Loss Function):
És una funció matemàtica que quantifica l’error entre la predicció del model i el valor real per a una mostra individual. Durant l’entrenament, el model calcula aquesta pèrdua per a cada exemple i l’objectiu és reduir-la al mínim.
Funció de Cost (Cost Function):
És la funció objectiu que avalua globalment l’error del model sobre el conjunt d’entrenament (o sobre un lot de mostres). Normalment és la mitjana de totes les pèrdues, però pot incloure termes addicionals com la regularització, per penalitzar models massa complexos. L’optimització del model consisteix a minimitzar aquesta funció.
Descens de Gradient (Gradient Descent):
Algoritme d’optimització iteratiu que ajusta els paràmetres del model en la direcció oposada al gradient de la funció de cost. L’objectiu és trobar els valors dels paràmetres que minimitzen l’error. És una tècnica fonamental per entrenar models en aprenentatge automàtic.
Propagació endavant (Forward Propagation):
Procés en una xarxa neuronal on les dades d’entrada passen capa per capa fins a la capa de sortida. Cada capa calcula una activació basada en els pesos i funcions d’activació, produint finalment la predicció o resultat del model.
Retropropagació (Backpropagation):
Algorisme fonamental en xarxes neuronals que permet ajustar els pesos del model aprenent dels errors. Primer, la xarxa fa una predicció (propagació endavant) i es calcula l’error comparant amb la resposta correcta. A continuació, aquest error es “propaga cap enrere” capa per capa, utilitzant la regla de la cadena per saber com afecta cada pes a l’error final.
Descens de Gradient Estocàstic (SGD):
Variant del descens de gradient que actualitza els paràmetres del model utilitzant mostres individuals o petits subconjunts (mini-batchs) del conjunt d’entrenament. Tot i introduir més soroll en les actualitzacions, sovint accelera l’entrenament i permet escapar de mínims locals.
Optimització (Optimization):
Procés matemàtic mitjançant el qual es troben els valors òptims dels paràmetres d’un model que minimitzen (o maximitzen) una funció objectiu, com la funció de cost. L’optimització és el nucli del procés d’aprenentatge en machine learning.
Validació Creuada (Cross-Validation):
Tècnica que divideix les dades en k parts (folds) i entrena el model k vegades, cada vegada deixant una part diferent per a validació i entrenant amb la resta. Això permet utilitzar tota la informació de les dades tant per entrenar com per validar, i ofereix una estimació més fiable del rendiment del model, especialment quan les dades són limitades.
Qualitats dels models
Sobreajustament (Overfitting):
Succeeix quan un model aprèn massa bé els detalls i el soroll del conjunt d’entrenament, fins al punt que perd la capacitat de generalitzar. Això es manifesta quan el rendiment en entrenament és alt, però el rendiment en validació o test és baix. S’acostuma a donar en models molt complexos o amb massa paràmetres entrenats amb poca quantitat de dades.
Subajustament (Underfitting):
Es dona quan el model és massa senzill o poc entrenat i no aconsegueix capturar els patrons rellevants de les dades, tant d’entrenament com de test. En aquest cas, el rendiment és baix a tots els conjunts de dades. Pot ser degut a un model massa simple, a un entrenament insuficient o a una selecció inadequada de característiques.
Compromís Biaix-Variància (Bias-Variance Tradeoff):
Principi fonamental que descriu l’equilibri entre dos tipus d’error:
- Biaix alt: model massa simple, que no capta bé els patrons (subajustament).
- Variància alta: model massa sensible a les dades d’entrenament (sobreajustament).
L’objectiu és trobar un model prou flexible per captar els patrons rellevants, però no tan complex que memoritzi el soroll.
Generalització (Generalization):
Capacitat d’un model d’oferir bones prediccions sobre dades que no ha vist durant l’entrenament. La generalització és la meta principal de l’aprenentatge automàtic i es mesura mitjançant el rendiment en conjunts de validació o test. Millorar-la implica trobar el model adequat, aplicar regularització i utilitzar dades representatives.
Explicabilitat (Explainability):
Capacitat de comprendre, justificar o interpretar el comportament d’un model: per què pren una decisió concreta o quines característiques hi tenen més pes. És especialment important en entorns regulats o sensibles (com salut o finances) i en models complexos com les xarxes neuronals, que poden actuar com a “caixes negres”.
Justícia (Fairness):
Propietat que busca assegurar que el model no discrimini ni tracti de forma desigual grups poblacionals segons característiques com gènere, raça, edat o origen. La justícia s’avalua mitjançant mètriques específiques i tècniques d’anàlisi d’equitat, i forma part de l’ètica en la intel·ligència artificial.
Robustesa (Robustness):
Habilitat del model per mantenir un comportament estable davant canvis inesperats o condicions adverses, com soroll a les dades, valors fora de rang o manipulacions intencionades (adversarial attacks). Un model robust no depèn de petits detalls irrelevants de l’entrada i respon de manera fiable en contextos diferents als d’entrenament.
Arquitectures de Xarxes
Xarxes Neuronals (Neural Networks):
Sistemes computacionals inspirats en el cervell humà formats per neurones artificials interconnectades. Cada neurona processa informació i la transmet a les següents. Són la base de molts sistemes d’intel·ligència artificial.
Aprenentatge Profund (Deep Learning):
Branca de l’aprenentatge automàtic basada en xarxes neuronals amb moltes capes (profundes). Aquestes capes permeten aprendre representacions jeràrquiques de les dades, des de característiques simples fins a conceptes complexos.
Xarxes Convolucionals (CNNs):
Un tipus de xarxa neuronal especialment dissenyada per processar dades visuals com imatges o vídeos. Fan servir filtres (convolucions) que detecten patrons visuals com vores, formes o textures.
Xarxes Recurrents (RNNs):
Xarxes dissenyades per tractar dades seqüencials, com textos, àudio o sèries temporals. Tenen una memòria interna que conserva informació de passos anteriors, la qual cosa els permet entendre el context dins d’una seqüència.
LSTM (Long Short-Term Memory):
Una variant de les RNN que utilitza una estructura interna amb “portes” per mantenir informació rellevant durant períodes més llargs. Resol problemes de dependències a llarg termini en dades seqüencials.
GRU (Gated Recurrent Unit):
Versió simplificada de les LSTM amb rendiment similar però menor complexitat computacional. També útil per processar dades seqüencials.
Codificadors automàtics (Autoencoders):
Xarxes neuronals que aprenen a codificar les dades d’entrada en una representació més compacta (codificació) i després reconstruir-les. Són útils per reduir la dimensionalitat, detectar anomalies o eliminar soroll.
Transformers:
Arquitectura basada en mecanismes d’atenció que permet tractar dades seqüencials sense recórrer a recurrència. Molt eficaç en tasques de llenguatge natural i visió artificial. Ha substituït en gran mesura les RNNs.
GANs (Generative Adversarial Networks):
Consten de dues xarxes que competeixen entre si: un generador que crea dades falses i un discriminador que intenta distingir-les de les reals. Són molt potents en la generació de continguts visuals realistes.
ResNet (Residual Networks):
Arquitectura que introdueix connexions residuals per facilitar l’entrenament de xarxes molt profundes. Ha estat clau en la millora del rendiment en classificació d’imatges.
ViT (Vision Transformers):
Adaptació dels Transformers al camp de la visió per ordinador. Tracten imatges com seqüències de fragments, eliminant la necessitat de convolucions.
Graph Neural Networks (GNNs):
Xarxes neuronals dissenyades per treballar amb dades representades com grafos, com xarxes socials, estructures químiques o sistemes de recomanació.
Mecanismes d’Aprenentatge
Tècniques de preprocessament
Tokenització (Tokenization):
Procés de divisió del text en unitats més petites i processables (tokens) com paraules, subparaules o caràcters, convertint-los en format numèric per al processament del model.
Enginyeria de Característiques (Feature Engineering):
Selecció, transformació i creació de característiques per millorar el model.
Escalat (Scaling):
Mapejar cada característica a un rang acotat definit (p. ex. [0,1] o [-1,1]; típicament amb Min‑Max). S’aplica a les dades d’entrada per fer comparables les escales.
Estandardització / z-score (Standardization):
Centrar cada característica (μ=0) i escalar per la seva desviació estàndard (σ=1); típicament amb StandardScaler. Independent per a cada feature.
Normalització (Normalization):
En sentit estricte, aplicar una transformació per aconseguir una propietat estadística concreta (p. ex. norma unitària, centrat). Col·loquialment sovint s’usa com a terme paraigua per escalat/estandardització.
Augmentació de Dades (Data Augmentation):
Procés de generar noves dades d’entrenament a partir de dades existents aplicant-hi modificacions (com girs, rotacions, soroll…). És molt útil en visió per ordinador per millorar la generalització.
Sobremostreig (Oversampling):
Tècnica per tractar dades desbalancejades que consisteix a augmentar el nombre d’exemples de les classes minoritàries per equilibrar el conjunt de dades. Es pot fer duplicant exemples existents o generant-ne de nous sintèticament.
Submostreig (Undersampling):
Mètode complementari al sobremostreig que consisteix a reduir el nombre de mostres de les classes majoritàries per igualar la distribució de classes.
Imputació (Imputation):
Tècnica per gestionar valors absents en les dades, substituint-los per valors calculats (mitjana, mediana, valor més freqüent) o mitjançant models predictius.
Codificació de Variables Categòriques:
Transformació de variables no numèriques en representacions numèriques. Inclou tècniques com one-hot encoding (vector binari) i label encoding (assignació d’enters únics).
Reducció de Dimensionalitat (Dimensionality Reduction):
Tècnica per disminuir el nombre de variables o característiques en un conjunt de dades, mantenint al màxim la informació rellevant. Pot fer-se amb mètodes estadístics (com PCA) o xarxes com els autoencoders.
Tècniques de Regularització
Regularització:
Conjunt de tècniques per prevenir el sobreentrenament (overfitting), penalitzant la complexitat del model o restringint la magnitud dels pesos. Inclou mètodes com L1, L2, dropout i weight decay.
Dropout:
Mètode de regularització que consisteix a desactivar de manera aleatòria algunes neurones durant l’entrenament per evitar dependències excessives i millorar la generalització del model.
Aturada Anticipada (Early Stopping):
Tècnica per evitar el sobreentrenament aturant l’entrenament del model quan el rendiment en les dades de validació comença a empitjorar.
Tècniques d’Aprenentatge
Normalització per Batch (Batch Normalization):
Tècnica que normalitza les activacions de cada capa durant l’entrenament per estabilitzar i accelerar el procés. Ajuda a reduir el problema del vanishing gradient, permet utilitzar taxes d’aprenentatge més altes i té efectes de regularització secundaris.
Ponderació per Classes (Class Weighting):
Tècnica d’entrenament que assigna pesos diferents a les classes durant el càlcul de la funció de pèrdua per compensar dades desbalancejades. Les classes minoritàries reben pesos més alts, fent que els seus errors tinguin més impacte en l’optimització del model sense modificar el conjunt de dades original.
Transfer Learning (Aprenentatge Transferit):
Consisteix a reutilitzar un model ja entrenat per resoldre un nou problema amb poques dades. S’aprofita el coneixement adquirit per una tasca similar (per exemple, amb xarxes com ResNet o BERT).
Ajust Fi (Fine-Tuning):
Variant del transfer learning on es reentrenen algunes parts del model preentrenat (normalment les últimes capes) perquè s’adaptin millor a la nova tasca.
Atenció (Attention):
Mecanisme que permet al model enfocar-se en parts específiques de l’entrada més rellevants per a la tasca. S’utilitza sovint per comparar dues seqüències i establir correspondències (per exemple, en traducció automàtica).
Atenció Auto-regressiva (Self-Attention):
Tipus d’atenció aplicat dins d’una mateixa seqüència, on cada element pot considerar els altres per captar dependències internes a llarg abast. És fonamental en arquitectures com els Transformers.
Intel·ligència Artificial Generativa i Models de Llenguatge
IA Generativa (Generative AI):
Sistemes d’intel·ligència artificial capaços de crear contingut original i realista (text, imatges, àudio, vídeo, codi) a partir de patrons apresos de grans volums de dades d’entrenament.
Models Generatius (Generative Models):
Tipus de models que aprenen la distribució de probabilitat de les dades d’entrenament per generar mostres noves i similars. Inclouen arquitectures com GANs, VAEs i models de difusió.
Models de Llenguatge (Language Models):
Models estadístics que aprenen la probabilitat de seqüències de paraules per predir el següent token i generar text coherent i contextualitzat.
Models de Llenguatge Grans (LLMs):
Models de llenguatge massius amb bilions de paràmetres, entrenats amb enormes corpus de text. Són capaços de realitzar múltiples tasques de NLP utilitzant arquitectures Transformer.
Tokenització (Tokenization):
Procés de divisió del text en unitats més petites i processables (tokens) com paraules, subparaules o caràcters, convertint-los en format numèric per al processament del model.
Embeddings:
Representacions vectorials denses que codifiquen paraules, frases o conceptes en un espai matemàtic multidimensional, capturant relacions semàntiques i sintàctiques.
Espai Vectorial (Vector Space):
Espai matemàtic multidimensional on es representen els embeddings, permetent calcular distàncies i similituds entre conceptes mitjançant operacions vectorials.
Similaritat Cosinus (Cosine Similarity):
Mètrica que mesura la similitud semàntica entre dos vectors calculant el cosinus de l’angle que formen, independent de la seva magnitud.
Prompting:
Tècnica d’interacció amb models generatius mitjançant instruccions, preguntes o exemples específics per guiar la generació cap al resultat desitjat. Una tècnica concreta és Chain-of-Thought.
Emmascarament (Masking):
Tècnica d’entrenament que consisteix a ocultar parts del text d’entrada perquè el model aprengui a predir les paraules emmascarades basant-se en el context.
Transferència Zero-shot i Few-shot:
Capacitat dels LLMs per realitzar tasques noves sense entrenament específic (zero-shot) o amb només uns pocs exemples (few-shot), aprofitant el coneixement adquirit durant l’entrenament.
Generació Multimodal (Multimodal Generation):
Capacitat de crear o combinar diferents tipus de contingut (text, imatges, àudio, vídeo) de manera coherent, permetent interaccions més riques entre modalitats diverses.
Generació Augmentada amb Recuperació (Retrieval-Augmented Generation – RAG):
Tècnica que combina un model de llenguatge amb un sistema de recuperació d’informació externa. Durant la inferència, el model consulta documents rellevants i utilitza aquest contingut per generar respostes més precises i actualitzades.
Àrees Aplicades i Tipus de Problemes
Classificació (Classification):
Assignació d’exemples a categories o classes predefinides mitjançant aprenentatge supervisat. Pot ser binària (dues classes) o multiclasse, i s’utilitza per organitzar i categoritzar dades de forma automàtica.
🟢 Exemple: Classificar clients com a “prospectes interessats” o “no interessats” en un producte per optimitzar campanyes de vendes.
Regressió (Regression):
Predicció de valors numèrics continus a partir de característiques d’entrada. Serveix per estimar quantitats com preus, temperatures o probabilitats.
🟢 Exemple: Estimar les vendes mensuals d’un producte basant-se en variables com pressupost de màrqueting, temporada i tendències passades.
Agrupament (Clustering):
Organització d’exemples similars en grups (clusters) sense etiquetes prèvies, mitjançant aprenentatge no supervisat. Permet descobrir patrons ocults dins les dades.
🟢 Exemple: Segmentar clients segons els seus hàbits de compra per dissenyar campanyes de màrqueting personalitzades.
Detecció d’Anomalies (Anomaly Detection):
Identificació de patrons, esdeveniments o observacions que es desvien del comportament normal per detectar fraus, errors o situacions inusuals.
🟢 Exemple: Detectar transaccions financeres inusuals que podrien indicar frau o errors en la comptabilitat.
Recomanació (Recommendation):
Suggeriment d’elements rellevants (productes, continguts, serveis, persones) basat en preferències, comportaments passats o similituds amb altres usuaris.
🟢 Exemple: Recomanar productes complementaris a un client en una botiga en línia per augmentar la facturació.
Anàlisi de Sèries Temporals (Time Series Analysis):
Estudi de dades ordenades cronològicament per identificar tendències, estacionalitats i patrons temporals, i per fer prediccions sobre el comportament futur.
🟢 Exemple: Preveure la demanda de productes estacional per ajustar l’inventari i optimitzar la cadena de subministrament.
Recuperació d’Informació (Information Retrieval):
Recerca i obtenció de documents o continguts rellevants d’una col·lecció gran a partir de consultes específiques d’usuari.
🟢 Exemple: Cercar contractes o documents legals rellevants dins d’una base de dades interna d’una empresa.
Generació de Contingut (Content Generation):
Creació automàtica de contingut original i coherent (text, imatges, àudio, vídeo, codi, etc.) mitjançant models generatius, amb aplicacions creatives i productives.
🟢 Exemple: Generar automàticament descripcions de productes per a un catàleg en línia que faciliti la venda i redueixi el temps de redacció.
Anàlisi de Sentiment:
Determinació automàtica de l’actitud, opinió o emoció expressada en textos, útil per comprendre la percepció pública sobre temes, productes o serveis.
🟢 Exemple: Analitzar opinions de clients sobre un producte en xarxes socials per detectar possibles millores o punts febles.
Traducció Automàtica:
Conversió automàtica de text d’un idioma a un altre per facilitar la comunicació multilingüe i l’accés global a la informació.
🟢 Exemple: Traduir automàticament informes comercials per a clients internacionals.
Reconeixement d’Imatges:
Identificació i classificació automàtica del contingut present en una imatge, amb aplicacions en catalogació, seguretat, diagnòstic mèdic, entre d’altres.
🟢 Exemple: Identificar productes en imatges per automatitzar processos de control d’estoc en magatzems.
Aprenentatge per Reforç (Reinforcement Learning):
Enfocament d’aprenentatge automàtic en què un agent aprèn a prendre decisions seqüencials mitjançant interacció amb un entorn. L’agent rep recompenses o penalitzacions segons les seves accions i aprèn estratègies òptimes per maximitzar la recompensa acumulada a llarg termini.
🟢 Exemple: Optimitzar la logística d’una empresa per reduir costos de transport aprenent la millor ruta i distribució de recursos.
Llista d’algoritmes
Algorismes Essencials
| Mètode / Algoritme | Tipus de problema | Descripció breu |
|---|---|---|
| Regressió lineal | Regressió | Ajusta un model lineal y = \beta_0 + \beta_1 x. |
| Regressió logística | Classificació (binària/multiclasse) | Estima la probabilitat d’una classe mitjançant la funció sigmoide (o multinomial en multiclasse). |
| Ridge / Lasso | Regressió | Regressió lineal amb penalització L2 (Ridge) o L1 (Lasso) per controlar l’overfitting. |
| Polynomial Features + Regressió | Regressió | Genera termes d’ordre superior com a part de l’enginyeria de característiques. |
| Arbre de decisió | Classificació / Regressió | Splits basats en guany d’informació o reducció d’error per generar regles d’arbre. |
| Random Forest | Classificació / Regressió | Conjunt d’arbres de decisió entrenats sobre submostres de dades i característiques. |
| Gradient Boosting | Classificació / Regressió | Seqüència d’arbres dèbils (XGBoost, LightGBM) que aprenen a corregir errors iterativament. |
| MLP (Multilayer Perceptron) | Classificació / Regressió | Xarxa neuronal feedforward bàsica per a tasques supervisades. |
| k-Nearest Neighbors (k-NN) | Classificació / Regressió | Predicció basada en els k veïns més propers segons una mètrica de distància. |
| Support Vector Machine (SVM) | Classificació / Regressió (SVR) | Hiperplà que maximitza el marge entre classes (o SVR per regressió). |
| Naive Bayes | Classificació | Model probabilístic que assumeix independència condicional entre característiques. |
| K-means | Clustering | Partitiona el dataset en k grups minimitzants la suma de distàncies quadràtiques. |
| PCA | Reducció de dimensionalitat | Projecció ortogonal per retenir la màxima variància amb menys components. |
| ARIMA / SARIMA | Sèries temporals | Models paramètrics per a sèries estacionàries i amb estacionalitat. |
Algorismes Avançats
| Mètode / Algoritme | Tipus de problema | Descripció breu |
|---|---|---|
| t-SNE | Reducció de dimensionalitat / Visualització | Visualització no lineal en 2D/3D; costós per a conjunts grans. |
| UMAP | Reducció de dimensionalitat / Visualització | Alternativa més ràpida i escalable a t-SNE. |
| Gaussian Mixture Models (GMM) | Clustering | Agrupa mitjançant una combinació de distribucions gaussianes; ofereix probabilitats de pertinença. |
| DBSCAN | Clustering | Agrupa regions d’alta densitat i detecta soroll sense definir k. |
| Hierarchical Clustering | Clustering | Construcció aglomerativa o divisiva d’un dendrograma de clústers. |
| Isolation Forest | Detecció d’anomalies | Aïlla punts en arbres aleatoris; bo per a dades d’alta dimensionalitat. |
| Local Outlier Factor (LOF) | Detecció d’anomalies | Mesura la densitat local al voltant de cada punt per identificar outliers. |
| Elliptic Envelope | Detecció d’anomalies | Pressuposa una distribució el·líptica per detectar outliers. |
| Autoencoder | Reducció de dimensionalitat / Detecció d’anomalies | Xarxa neuronal que aprèn a comprimir i reconstruir; útil per a detecció d’outliers. |
| Collaborative Filtering | Recomanació | Suggeriments basats en similituds de comportament entre usuaris o ítems. |
| Matrix Factorization (ALS, SVD) | Recomanació | Aprenentatge de factors latents que representen usuaris i ítems en un espai de baixa dimensió. |
| Prophet | Sèries temporals | Biblioteca de Meta per a forecasts amb tendència i estacionalitat automàtiques. |
| RNN / LSTM / GRU | Seqüències (NLP, sèries temporals) | Xarxes neuronals recurrents per modelar dependències seqüencials. |
| Transformer (BERT, GPT) | Seqüència (NLP, generació de contingut) | Arquitectura basada en atenció; excel·lent en traducció, classificació i generació de text. |
| Ensembles: Stacking / Blending | Classificació / Regressió | Combina models heterogenis (p. ex. regressió + arbres) per millorar el rendiment. |
| Q-Learning / DQN | Reinforcement Learning | Algoritmes de valor per aprendre polítiques en entorns seqüencials. |
| Policy Gradients (REINFORCE, PPO) | Reinforcement Learning | Aprenentatge directe de polítiques sovint amb xarxes neuronals profundes. |
Conceptes matemàtics
Fonaments
| Concepte | Definició |
|---|---|
| Mitjana i mediana | Mitjana: suma de tots els valors dividit pel nombre de valors. Mediana: valor central quan ordenem les dades. Ús en ML: entendre la distribució de les dades, detectar outliers. |
| Escalars, vectors, matrius i tensors | Un escalar és un número sol (ex: temperatura = 25°C). Un vector és una llista ordenada de números (ex: coordenades [x, y]). Una matriu és una taula bidimensional de números (ex: imatge en escala de grisos). Un tensor és una generalització en més dimensions (ex: imatge RGB: ample × alt × 3 canals de color). |
| Funcions i derivades | Funció: mapeja entrades a sortides f(x) → y. Derivada: indica si una funció puja o baixa, i com de ràpid (com la pendent d’un turó). Derivades parcials: canvi respecte una variable mantenint les altres constants. |
| Logaritmes | log_b(x) respon “a quina potència he d’elevar b per obtenir x?”. Propietats útils: log(ab) = log(a) + log(b). Ús en ML: funcions de pèrdua (cross-entropy), softmax, evitar overflow numèric. |
Àlgebra lineal
| Concepte | Definició |
|---|---|
| Producte escalar (dot product) | Operació que combina dos vectors de la mateixa mida i retorna un escalar. Geomètricament parlant, és alt si apunten en la mateixa direcció, zero si són perpendiculars. Ús en ML: combinar pesos amb característiques en xarxes neuronals. |
| Norma (norm) | Mesura la “longitud” o magnitud d’un vector. L1 norm: suma dels valors absoluts. L2 norm: arrel quadrada de la suma dels quadrats. Ús en ML: normalització de dades i regularització. |
| Multiplicació matrius | Operació que combina dues matrius m \times n i n \times p resultant m \times p. Cada element del resultat és el producte escalar (dot product) d’una fila de la primera matriu amb una columna de la segona. Ús en ML: transforma dades d’entrada aplicant pesos per calcular prediccions en xarxes neuronals. |
| Distàncies | Euclidiana: distància recta entre punts (L2). Manhattan: suma de diferències absolutes (L1). Cosinus: mesura l’angle entre vectors, ignora la magnitud. Ús en ML: k-NN, clustering, mesura de similaritat. |
Probabilitat i estadística
| Concepte | Definició |
|---|---|
| Probabilitat | Mesura de la incertesa: P(A) indica la probabilitat que passi A, entre 0 i 1. Ús en ML: classificació probabilística, incertesa de prediccions. |
| Probabilitat condicional | Probabilitat que passi A sabent que ha passat B: P(A|B). Ús en ML: classificadors bayesians, arbres de decisió. |
| Teorema de Bayes | El Teorema de Bayes actualitza la probabilitat d’un fet A donada l’evidència B: P(A|B)=P(B|A)·P(A)/P(B), combinant prior, versemblança i evidència total. |
| Esperança i variància | Esperança o mitjana (μ): valor mitjà esperat d’una variable aleatòria. Variància (σ²): mesura la dispersió respecte la mitjana. Ús en ML: normalització, inicialització de pesos. |
| Correlació | Mesura de dependència lineal entre dues variables. Coeficient de Pearson: varia entre -1 (correlació negativa perfecta) i +1 (correlació positiva perfecta), 0 indica independència lineal. Ús en ML: selecció de característiques, detecció de multicol·linearitat. |
| Distribucions de probabilitat | Modelen com es distribueixen els valors. Normal/Gaussiana: forma de campana, molt comuna. Bernoulli: èxit/fracàs. Categòrica: múltiples classes. Ús en ML: modelatge de dades, inicialització. |
Optimització i entrenament
| Concepte | Definició |
|---|---|
| Gradient | Vector que indica la direcció de màxim creixement d’una funció. En ML, apunta cap on incrementar els pesos per augmentar l’error, per això anem en direcció oposada. |
| Descens pel gradient | Algorisme que minimitza una funció seguint el gradient negatiu. Actualitza pesos iterativament: pes_nou = pes_antic - taxa_aprenentatge * gradient. Base de l’entrenament de models. |
| Funcions de pèrdua (loss) | Mesuren l’error del model. MSE: per regressió. Cross-entropy: per classificació. Objectiu: minimitzar aquesta funció durant l’entrenament. |
Models i arquitectures
| Concepte | Definició |
|---|---|
| Funcions lineals | Equacions de la forma y = mx + b (2D) o y = w₁x₁ + w₂x₂ + \ldots + b (multidimensional). Limitació: només poden separar dades linealment separables. |
| Preactivació | Valor calculat en una neurona com a combinació lineal de les entrades i els pesos, abans d’aplicar-hi la funció d’activació. Representa la “entrada neta” que rep la neurona. Matemàticament, s’expressa com z = w \cdot x + b , on w són els pesos, x les entrades i b el biaix. |
| Funcions d’activació | Funcions no lineals que s’apliquen a la preactivació de cada neurona. Transformen el valor z (la preactivació) en una sortida a , que es propaga cap a la següent capa. Permeten a la xarxa neuronal aprendre patrons complexos i no lineals. Exemples: ReLU ( \max(0, z) ), sigmoide ( \frac{1}{1 + e^{-z}} ), tanh. |
Avaluació i generalització
| Concepte | Definició |
|---|---|
| Escalat i estandardització de dades | Ajustar escales de variables. Min‑Max (escalat): mapeja a [0,1]. Z‑score (estandardització): mitjana 0, desviació 1. Necessari: molts algoritmes són sensibles a l’escala. |
| Mètriques d’avaluació | Classificació: accuracy, precision, recall, F1-score. Regressió: MSE, MAE, RMSE, R². Matriu de confusió: visualitza errors de classificació. |
| MSE (Mean Squared Error) | Error quadràtic mitjà. Calcula la mitjana dels quadrats de les diferències entre prediccions i valors reals. Penalitza més els errors grans. |
| RMSE (Root Mean Squared Error) | Arrel quadrada del MSE. Té les mateixes unitats que la variable objectiu, facilitant la interpretació. Manté la propietat de penalitzar errors grans. |
| MAE (Mean Absolute Error) | Error absolut mitjà. Calcula la mitjana de les diferències absolutes entre prediccions i valors reals. Més robust a outliers que MSE. |
| Coeficient de determinació (R²) | Mètrica per a regressió que indica la proporció de variància de la variable dependent explicada pel model. Varia entre 0 (el model no explica millor que la mitjana) i 1 (ajust perfecte). Valors negatius indiquen que el model és pitjor que predir simplement la mitjana. |
| Corba ROC i AUC | Per a la classificació binària. ROC: gràfic que mostra la relació entre taxa de vertaders positius (recall) vs. falsos positius a diferents llindars. AUC: àrea sota la corba, mesura la capacitat discriminativa del model (0.5 = aleatori, 1.0 = perfecte). |
| Sobreajustament i regularització | Sobreajustament: model memoritza dades d’entrenament però generalitza malament. Regularització: tècniques per evitar-ho (L1/L2, dropout). Compromís biaix-variance. |
| Dimensionalitat | Maledicció de la dimensionalitat: en espais d’alta dimensió, les dades es tornen disperses. Reducció: PCA, t-SNE. Feature selection: triar variables rellevants. |
Abreviatures
| Abreviatura | Forma completa en català / anglès | Descripció breu |
|---|---|---|
| AE | Autoencoder | Xarxa neuronal que aprèn a codificar i reconstruir dades; útil per a anomalies. |
| AI | Intel·ligència Artificial (Artificial Intelligence) | Camp que busca crear sistemes que simulen la intel·ligència humana. |
| ALS | Alternating Least Squares | Mètode de factorització de matrius per a recomanacions col·laboratives. |
| ARIMA | AutoRegressive Integrated Moving Average | Model per a sèries temporals amb autoregressió, diferenciació i mitjana mòbil. |
| BERT | Bidirectional Encoder Representations from Transformers | Transformer preentrenat per a representació de text bidireccional. |
| CNN | Xarxa Neuronal Convolucional (Convolutional Neural Network) | Xarxa especialitzada en processar dades visuals. |
| DBSCAN | Density-Based Spatial Clustering of Applications with Noise | Clustering basat en densitat; detecta soroll sense definir nombre de clústers. |
| DL | Aprenentatge Profund (Deep Learning) | Subcamp de ML que usa xarxes neuronals profundes. |
| DQN | Deep Q-Network | Q-Learning amb xarxa neuronal per estimar la funció de valor. |
| DT | Arbre de decisió (Decision Tree) | Model que fa particions recursives basades en guany d’informació o gini. |
| GAN | Xarxa Generativa Adversarial (Generative Adversarial Network) | Arquitectura per generar dades noves mitjançant una competició entre models. |
| GBM | Gradient Boosting Machine | Seqüència d’arbres dèbils entrenats en residus per reduir l’error. |
| GMM | Gaussian Mixture Models | Model probabilístic que ajusta clústers amb combinacions de gaussianes. |
| GPU | Unitat de Processament Gràfic (Graphics Processing Unit) | Hardware utilitzat per accelerar càlculs paral·lels, especialment en DL. |
| GPT | Generative Pre-trained Transformer | Transformer autogeneratiu per a generació de text i tasques de seqüència. |
| GRU | Gated Recurrent Unit | Variante d’RNN amb portes d’actualització i reinicialització. |
| KNN | k-Nearest Neighbors | Predicció basada en els k veïns més propers segons una mètrica de distància. |
| LLM | Model Gran de Llenguatge (Large Language Model) | Models de llenguatge amb molts paràmetres, capaços de generar text natural. |
| LGBM | LightGBM | Biblioteca ràpida de gradient boosting basada en histogrammes. |
| LOF | Local Outlier Factor | Mesura la densitat local per identificar punts que difereixen del veïnat. |
| LSTM | Long Short-Term Memory | RNN amb cel·les de memòria que aprenen dependències a llarg termini. |
| ML | Aprenentatge Automàtic (Machine Learning) | Subcamp d’IA que fa que els sistemes aprenguin de dades. |
| MLP | Multilayer Perceptron | Xarxa neuronal feedforward bàsica per a tasques supervisades. |
| NB | Naive Bayes | Classificador probabilístic que assumeix independència condicional. |
| PCA | Principal Component Analysis | Reducció de dimensionalitat per projecció ortogonal de màxima variància. |
| PPO | Proximal Policy Optimization | Algoritme de Policy Gradient que controla la magnitud de l’actualització. |
| RNN | Xarxa Neuronal Recurrent (Recurrent Neural Network) | Xarxa especialitzada en dades seqüencials. |
| RF | Bosc aleatori (Random Forest) | Conjunt d’arbres de decisió entrenats sobre submostres aleatòries. |
| SARIMA | Seasonal ARIMA | Extensió d’ARIMA que inclou components estacionals. |
| SDG | Stochastic Gradient Descent | Algoritme d’optimització per ajustar pesos. |
| SVD | Singular Value Decomposition | Factorització de matrius per a reducció de dimensionalitat i recomanacions. |
| SVM | Màquina de Vectors de Suport (Support Vector Machine) | Algoritme supervisat per classificació i regressió. |
| t-SNE | t-Distributed Stochastic Neighbor Embedding | Mètode no lineal per a visualització de dades en 2D/3D preservant veïnats. |
| TPU | Unitat de Processament Tensorial (Tensor Processing Unit) | Processador específic per xarxes neuronals desenvolupat per Google. |
| UMAP | Uniform Manifold Approximation and Projection | Alternativa a t-SNE més ràpida i escalable per a reducció de dimensionalitat. |
| XGBoost | eXtreme Gradient Boosting | Implementació eficient de gradient boosting amb regularització. |
Estadística i Probabilitat bàsiques
- Probabilitat: Mesurant la Incertesa
- Variables aleatòries
- Mitjana i dispersió
- Distribucions importants
- El Teorema del límit central
- Estadística inferencial
- Tests d’hipòtesi
- Intervals de confiança
- Regressió des de la probabilitat
- Mesurant la incertesa i la informació
- Mostreig i Bootstrapping
- Conceptes avançats per a ML
- Consells pràctics
- Glossari
Probabilitat: Mesurant la Incertesa
Què és realment la probabilitat?
La probabilitat és una mesura numèrica entre 0 i 1 que expressa la plausibilitat que succeeixi un esdeveniment. Una probabilitat de 0 significa impossible, mentre que 1 significa certesa absoluta.
Imagina que llances una moneda. No saps si sortirà cara o creu, però saps que hi ha dues opcions possibles. La probabilitat és simplement una manera de posar un número a aquesta incertesa.
Escala de probabilitat:
- 0 = Impossible (0%)
- 0.5 = Tan probable com no probable (50%)
- 1 = Segur que passa (100%)
Exemple pràctic: Si tens una bossa amb 7 boles vermelles i 3 boles blaves:
- Probabilitat de treure vermella = 7/10 = 0.7 (70%)
- Probabilitat de treure blava = 3/10 = 0.3 (30%)
Notació bàsica
- P(A) = “la probabilitat que passi A”
- P(A o B) o P(A ∪ B) = “la probabilitat que passi A, o B, o ambdós”
- P(A i B) o P(A ∩ B) = “la probabilitat que passin A i B alhora”
Les dues regles d’or
1. Regla de la suma (per coses que NO poden passar alhora):
Si tens dues opcions que s’exclouen mútuament (com cara i creu), sumes les probabilitats:
P(cara o creu) = P(cara) + P(creu) = 0.5 + 0.5 = 1
2. Regla del producte (per coses independents):
Si vols saber la probabilitat que passin dues coses independents, multipliques:
P(dos caps seguits) = P(cap) × P(cap) = 0.5 × 0.5 = 0.25
Exemple visual: Imagina llançar dues monedes. Hi ha 4 resultats possibles:
- Cara-Cara (25%)
- Cara-Creu (25%)
- Creu-Cara (25%)
- Creu-Creu (25%)
Probabilitat condicional: “I si ja sé alguna cosa?”
Ara imagina que algú et diu: “La primera moneda ha sortit cara”. Això canvia les probabilitats!
P(A|B) = “la probabilitat d’A sabent que B ja ha passat”
P(A|B) = P(A i B) / P(B)
Exemple intuïtiu: En un grup de 100 persones:
- 60 són dones, 40 són homes
- De les dones, 20 porten ulleres
- Dels homes, 10 porten ulleres
Si escollim una persona amb ulleres, quina probabilitat hi ha que sigui dona?
- P(dona|ulleres) = P(dona i ulleres)/P(ulleres) = 20/(20+10) = 20/30 = 67%
Per què importa en ML? Quan classifiquem (spam/no spam, gat/gos), constantment preguntem: “donades aquestes característiques, quina és la probabilitat d’aquesta classe?”
Teorema de Bayes
Aquest és possiblement el teorema més important del machine learning:
P(A|B) = P(B|A) × P(A) / P(B)
Versió amb paraules normals:
P(hipòtesi|dades) = P(dades|hipòtesi) × P(hipòtesi) / P(dades)
Exemple del món real - Detecció de spam:
Vols saber: “Aquest email és spam donat que conté la paraula ‘gratis’?”
- P(spam|“gratis”) = el que vols saber
- P(“gratis”|spam) = en quants spams apareix “gratis” (pots comptar-ho!)
- P(spam) = quants emails són spam en general (també pots comptar-ho!)
- P(“gratis”) = en quants emails apareix “gratis” (també comptable!)
Bayes et permet calcular el primer a partir dels altres tres, que són més fàcils d’obtenir de les dades!
Per què és tan potent? Perms actualitzar les nostres creences quan tenim noves dades. És la base de molts algoritmes de ML com Naive Bayes.
Variables aleatòries
Què és una variable aleatòria?
És simplement una manera de convertir resultats en números. Per exemple:
- Llançar una moneda → 0 (creu) o 1 (cara)
- Comptar clients en una botiga → 0, 1, 2, 3, …
- Mesurar temperatura → 18.5°C, 19.2°C, …
Dues famílies: Discretes i Contínues
Variables DISCRETES: prenen valors separats i comptables (com el nombre de clients, el resultat d’un dau). Tenen una funció de massa de probabilitat (PMF): P(X = x)
- Nombre de likes en un post (0, 1, 2, 3…)
- Resultat d’un dau (1, 2, 3, 4, 5, 6)
- Número de vendes en un dia
Variables CONTÍNUES: poden prendre qualsevol valor dins d’un interval (com l’altura, el temps). Tenen una funció de densitat de probabilitat (PDF): f(x)
- Alçada d’una persona (1.75m, 1.8023m…)
- Temps fins que carrega una web (2.3 segons, 2.31 segons…)
- Temperatura
Diferència clau per entendre:
- Discretes: pots fer una llista completa de valors possibles
- Contínues: entre dos valors sempre n’hi ha un altre (infinits valors possibles)
Com descriure una variable aleatòria?
Per discretes - Taula de probabilitats:
| Resultat dau | Probabilitat |
|---|---|
| 1 | 1/6 |
| 2 | 1/6 |
| … | … |
| 6 | 1/6 |
Per contínues - Corba de densitat:
Imagina un histograma molt suavitzat. La àrea sota la corba entre dos valors et dona la probabilitat.
Exemple: L’alçada de persones adultes segueix una corba amb forma de campana (més gent al voltant de 1.70m, menys en els extrems).
Mitjana i dispersió
La mitjana (esperança)
És el valor que esperaries “de mitjana” si repetissis l’experiment moltes vegades.
Per variables discretes:
Mitjana = suma de (cada valor × la seva probabilitat)
Exemple dau honest:
Mitjana = 1×(1/6) + 2×(1/6) + 3×(1/6) + 4×(1/6) + 5×(1/6) + 6×(1/6) = 3.5
Fixa’t: mai sortirà 3.5 en un llançament, però és el valor “central” de tots els resultats!
Per variables contínues: És el “centre de gravetat” de la distribució.
Propietat útil per calcular:
- Mitjana(2X + 3) = 2×Mitjana(X) + 3
Això vol dir que pots calcular mitjanes de transformacions fàcilment!
La variància: “Com de dispersos estan els valors?”
La variància mesura com de lluny estan típicament els valors de la mitjana.
Variància = mitjana de (cada valor - mitjana)²
Per què elevem al quadrat? Perquè així les desviacions positives i negatives no s’anul·len!
Exemple intuïtiu:
Dos casinos:
- Casino A: guanyes 10€ o perds 10€ (mitjana = 0€)
- Casino B: guanyes 1000€ o perds 1000€ (mitjana = 0€)
Ambdós tenen mitjana 0, però el Casino B té molta més variància (és més arriscat!).
Desviació estàndard: La variància en “unitats normals”
Desviació estàndard = √Variància
Per què és útil? Té les mateixes unitats que la variable original.
Si mesures alçades en centímetres:
- La variància està en cm² (difícil d’interpretar)
- La desviació estàndard està en cm (fàcil d’interpretar!)
Regla pràctica: En molts casos, el 68% dels valors cauen dins de “mitjana ± 1 desviació estàndard”.
Covariància i Correlació: “Van juntes?”
Covariància: Mesura si dues variables tendeixen a créixer juntes o en direccions oposades.
- Covariància positiva: quan una puja, l’altra tendeix a pujar
- Covariància negativa: quan una puja, l’altra tendeix a baixar
- Covariància zero: no hi ha relació lineal clara
Problema: La covariància depèn de les unitats de mesura!
Solució: Correlació - és la covariància normalitzada, sempre entre -1 i +1:
Correlació = Covariància / (desv.std(X) × desv.std(Y))
Interpretació visual:
- ρ = +1: Relació lineal perfecta positiva (línia /)
- ρ = -1: Relació lineal perfecta negativa (línia )
- ρ = 0: No hi ha relació lineal (núvol de punts)
- ρ = +0.7: Relació positiva forta però no perfecta
⚠️ Advertència important: Correlació zero NO significa que no hi ha relació! Només que no n’hi ha de lineal. Podria haver-hi una relació corba o qualsevol altra.
Exemple clàssic: Alçada i temperatura exterior. No estan correlacionades, però hi ha estacions (relació no-lineal)!
Distribucions importants
Distribució Normal (Gaussiana): la reina
Com la reconeixeràs: Forma de campana simètrica.
Paràmetres:
- μ (mu): La mitjana (centre de la campana)
- σ (sigma): La desviació estàndard (com d’ampla és)
La regla 68-95-99.7 (memoritza-la!):
- 68% dels valors estan dins de μ ± σ
- 95% estan dins de μ ± 2σ
- 99.7% estan dins de μ ± 3σ
Exemple pràctic: L’alçada d’homes adults:
- Mitjana: 175cm
- Desviació: 7cm
- Això significa que el 68% mesuren entre 168cm i 182cm
Per què és tan important?
-
Molts fenòmens naturals la segueixen: alçades, errors de mesura, soroll en sensors…
-
Teorema del límit central: quan sumes moltes variables aleatòries, el resultat tendeix a ser normal, fins i tot si les variables originals no ho són!
-
Matemàticament tractable: facilita molts càlculs
-
Base de molts algoritmes ML: regressió lineal, LDA, xarxes neuronals amb certs activacions…
En ML: Quan normalitzes dades (z-score), estàs convertint-les a una normal amb μ=0 i σ=1.
Distribució Bernoulli: l’experiment més simple
Situació: Un únic intent amb dos resultats possibles (èxit/fracàs, sí/no, 0/1).
Paràmetre:
- p: probabilitat d’èxit
Exemples:
- Llançar una moneda (p = 0.5)
- Click o no-click en un anunci (p = probabilitat de click)
- Client compra o no compra (p = taxa de conversió)
Propietats:
- Mitjana = p
- Variància = p(1-p)
Curiositat: La variància és màxima quan p=0.5 (màxima incertesa!)
Distribució Binomial: Bernoullis repetides
Situació: Repeteixes un experiment Bernoulli n vegades independentment.
Pregunta: Quants èxits obtinc en total?
Paràmetres:
- n: nombre d’intents
- p: probabilitat d’èxit en cada intent
Exemple: Llances 10 monedes. Quantes surten cara?
- n = 10, p = 0.5
- Mitjana esperada = np = 10 × 0.5 = 5 cares
- Però podries obtenir 3, 4, 5, 6, 7… amb diferents probabilitats
Propietats:
- Mitjana = n × p
- Desviació estàndard = √(np(1-p))
En ML: Útil per modelar tasques de classificació binària repetides.
Distribució de Poisson: comptar esdeveniments rars
Situació: Comptes quants esdeveniments passen en un interval fix (temps, espai…).
Característiques:
- Els esdeveniments són independents
- Passen a una taxa mitjana constant (λ, “lambda”)
- Són relativament rars
Paràmetre:
- λ: nombre mitjà d’esdeveniments en l’interval
Exemples:
- Número de clients que entren en una botiga per hora (λ = 15)
- Número de typos en una pàgina (λ = 2)
- Número de cridades a un call center per minut (λ = 3.5)
Propietat curiosa: Mitjana = Variància = λ
En ML: Útil per dades de recompte, especialment en processament de llenguatge natural (comptar paraules) i anàlisi de sèries temporals.
Distribució Exponencial: “quan arribarà el proper?”
Situació: Mesures el temps fins al proper esdeveniment en un procés de Poisson.
Paràmetre:
- λ: taxa d’esdeveniments (esdeveniments per unitat de temps)
Exemples:
- Temps fins a la propera crida al call center
- Temps de vida d’una bombeta
- Temps fins al proper terratrèmol
Propietat especial: “Sense memòria” - la probabilitat del proper esdeveniment no depèn de quant temps ha passat.
Exemple intuïtiu: Si una bombeta ha durat 1000 hores, la probabilitat que duri 100 hores més és la mateixa que al principi! (Això no és realista per bombetes reals, però sí per alguns processos).
El Teorema del límit central
La idea màgica
Enunciat en paraules simples:
Si agafes moltes mostres aleatòries i calcules la seva mitjana, aquestes mitjanes seguiran una distribució normal, independentment de com sigui la distribució original!
Exemple pas a pas
Imagina que tens una urna amb boles amb números de l’1 al 6 (com un dau). La distribució és plana (tots els valors igual de probables), NO és normal.
Experiment:
- Treu 30 boles (amb reemplaçament) i calcula la mitjana
- Repeteix això 1000 vegades
- Dibuixa un histograma de totes aquestes mitjanes
Resultat màgic: L’histograma de mitjanes tindrà forma de campana (normal)! Encara que la distribució original era plana.
Per què importa?
-
Justifica l’ús de la normal arreu
Molts fenòmens són sumes o mitjanes de molts petits efectes independents:
- El teu pes = genètica + dieta + exercici + stress + son + …
- Error de mesura = error de sensor + vibració + temperatura + …
- Nota d’examen = coneixement + sort en preguntes + estat d’ànim + …
Com que són sumes, tendeixen a ser normals!
-
Permet fer estadística amb mostres
Encara que no sàpigues la distribució de la població, les mitjanes de mostres grans seran (aproximadament) normals. Això permet fer tests d’hipòtesi i intervals de confiança!
-
Explica per què funciona normalitzar dades
En ML, sovint normalitzem les dades assumint normalitat. El TCL ens diu que això sovint és raonable!
Quanta dades necessito?
Regla empírica: Amb n ≥ 30, la distribució de mitjanes ja és força normal, fins i tot si la distribució original és estranya.
Si la distribució original ja és simètrica, potser n’hi ha prou amb n ≥ 10.
Estadística inferencial
El problema fonamental
Vols saber alguna cosa sobre una població (tots els clients, tots els usuaris, totes les peces fabricades), però només pots observar una mostra (uns quants).
Pregunta: Com pots fer afirmacions sobre la població basant-te només en la mostra?
Estimadors: les nostres millors conjectures
Un estimador és una fórmula que usa les dades de la mostra per “endevinar” un valor poblacional.
Exemple clàssic:
- Mitjana poblacional (μ): desconeguda
- Mitjana mostral (x̄): Σxᵢ/n - la pots calcular!
- Usem x̄ com a estimador de μ
Propietats d’un bon estimador
1. No esbiaixat: de mitjana, encerta
Si repeteixes el mostreig moltes vegades, la mitjana de tots els estimadors hauria de ser el valor real.
2. Consistent: millora amb més dades
Amb mostres més grans, l’estimador s’acosta més al valor real.
3. Eficient: té poca variància
Entre dos estimadors no esbiaixats, preferim el que té menys variabilitat.
Exemple intuïtiu d’esbiaixament
Imagina que vols estimar l’alçada mitjana de la població:
- Estimador esbiaixat: Només mesures gent que passa per una botiga de roba de talla gran → sobreestimaràs!
- Estimador no esbiaixat: Esculls persones aleatòriament de tota la població
Màxima versemblança: “què fa més probables les dades?”
La idea: escull el paràmetre que fa que les dades observades siguin més probables.
Exemple senzill:
Tens una moneda i la llances 10 vegades:
- Resultat: 7 cares, 3 creus
Quin valor de p (probabilitat de cara) fa aquest resultat més probable?
- Si p = 0.5: probabilitat d’observar 7 cares = moderat
- Si p = 0.7: probabilitat d’observar 7 cares = ALT
- Si p = 0.9: probabilitat d’observar 7 cares = baix
L’estimador de màxima versemblança seria p = 0.7 (que és exactament 7/10!)
En ML: Gairebé tots els algoritmes d’aprenentatge usen aquest principi! Quan entrenes una xarxa neuronal, estàs buscant els pesos que fan més probables les dades d’entrenament.
Tests d’hipòtesi
És real o és casualitat?
El plantejament
Tens una sospita (hipòtesi) sobre el món. Vols saber si les dades donen suport a aquesta sospita o si podria ser només casualitat.
Estructura:
- H₀ (hipòtesi nul·la): L’afirmació “avorrida” o per defecte (“no hi ha efecte”, “no hi ha diferència”)
- H₁ (hipòtesi alternativa): L’afirmació que creus/vols provar
Exemple concret
Situació: Has fet un canvi a la web (nou botó). Vols saber si millora les conversions.
- H₀: El nou botó NO canvia la taxa de conversió (diferència = 0)
- H₁: El nou botó SÍ canvia la taxa de conversió (diferència ≠ 0)
Dades:
- Abans: 100 conversions de 1000 visites (10%)
- Després: 130 conversions de 1000 visites (13%)
Pregunta: Aquesta diferència del 3% és real o podria haver passat per casualitat?
El p-valor
Què tan estrany és això?
Definició intuïtiva:
Si H₀ fos certa (el botó realment no fa res), quina probabilitat hi hauria d’observar una millora del 3% (o més) només per atzar?
Això és el p-valor.
Interpretació:
- p-valor petit (< 0.05): seria molt estrany veure aquests resultats per pur atzar. Conclusió: probablement el canvi SÍ té un efecte real.
- p-valor gran (> 0.05): aquests resultats podrien passar fàcilment per casualitat. Conclusió: no tenim prou evidència per dir que hi ha un efecte real.
Al nostre exemple del botó:
Si p-valor = 0.03 → Només un 3% de probabilitat que aquesta diferència sigui casualitat
Decisió pràctica:
- p < 0.05 → Rebutgem H₀ → Mantenim el nou botó
- p > 0.05 → No rebutgem H₀ → Potser tornem a l’antic
Analogia: És com preguntar “Quina probabilitat hi ha de trobar 10 cares seguides en 10 llançaments d’una moneda justa?”
- Resposta: 0.001 (molt petit!)
- Conclusió: Probablement la moneda no és justa
Llindar de significació (α)
Habitualment α = 0.05 (5%)
Decisió:
- Si p-valor < 0.05 → Rebutgem H₀ (“Hi ha efecte!”)
- Si p-valor ≥ 0.05 → No rebutgem H₀ (“No hi ha prou evidència”)
IMPORTANT: No rebutjar H₀ NO és el mateix que “acceptar” H₀. Simplement diem que no tenim prou evidència.
Els dos tipus d’error
Error tipus I (fals positiu):
- Rebutges H₀ quan és certa
- “Creus que hi ha efecte, però no n’hi ha”
- Probabilitat = α (per això escollim α petit!)
Error tipus II (fals negatiu):
- No rebutges H₀ quan és falsa
- “No detectes un efecte que SÍ existeix”
- Probabilitat = β
En ML: Aquests errors es relacionen directament amb:
- Fals Positiu = model diu “sí” quan hauria de dir “no”
- Fals Negatiu = model diu “no” quan hauria de dir “sí”
Exemple mèdic:
- Error Tipus I: Diagnostiques malaltia quan el pacient està sa
- Error Tipus II: No diagnostiques malaltia quan el pacient està malalt
Tradeoff: Reduir un error generalment augmenta l’altre!
Intervals de confiança
La idea intuïtiva
En comptes de dir “la mitjana és exactament 175cm”, diem “estic 95% confiat que la mitjana està entre 172cm i 178cm”.
Què significa “95% de confiança”?
Interpretació correcta:
Si repeteixes el mostreig moltes vegades i calcules un interval de confiança cada vegada, el 95% d’aquests intervals contindran el valor poblacional real.
Interpretació INCORRECTA (però comuna):
“Hi ha un 95% de probabilitat que el valor real estigui dins l’interval” ❌
El valor real és fix! La incertesa està en el nostre interval, no en el paràmetre.
Com es calcula? (cas simple)
Per a una mitjana, amb mostra gran:
Interval 95% = x̄ ± 1.96 × (s/√n)
On:
- x̄ = mitjana mostral
- s = desviació estàndard mostral
- n = tamany de la mostra
- 1.96 = valor crític per al 95% (ve de la distribució normal)
Observacions importants:
- L’interval és més estret amb més dades (√n al denominador)
- L’interval és més ample amb més variabilitat (s al numerador)
- Per a 99% de confiança, uses 2.58 en comptes de 1.96 (interval més ample = més segur)
Exemple pràctic
Mesures l’alçada de 100 estudiants:
- Mitjana = 170cm
- Desviació estàndard = 10cm
Interval 95%:
170 ± 1.96 × (10/√100)
= 170 ± 1.96 × 1
= 170 ± 1.96
= [168.04, 171.96]
Conclusió: Estem 95% confiats que l’alçada mitjana de tots els estudiants està entre 168cm i 172cm.
Per què és útil en ML?
- Comunicar incertesa: No només dius “el model prediu 42”, sinó “el model prediu 42 amb interval [38, 46]”
- Comparar models: Pots veure si les diferències són significatives o només soroll
- Avaluar fiabilitat: Intervals amples indiquen alta incertesa
Regressió des de la probabilitat
Què és realment la regressió?
Pregunta bàsica: Vull predir Y (preu d’una casa) a partir de X (metres quadrats).
Idea de regressió: Assumim que hi ha una relació lineal + soroll aleatori:
Y = β₀ + β₁X + ε
On:
- β₀ = intersecció (preu quan X=0)
- β₁ = pendent (quant puja Y per cada unitat de X)
- ε = error aleatori (tot el que no expliquem)
La part probabilística
Assumpcions clau:
- Els errors són aleatoris: ε ~ Normal(0, σ²)
- Els errors són independents: Un error no influeix l’altre
- La variància és constant: σ² és la mateixa per tots els valors de X
Traducció intuïtiva:
Per cada valor de X, Y segueix una distribució normal:
- Centre: β₀ + β₁X (la línia de regressió)
- Amplada: σ (la mateixa per a tots els X)
Visualitza-ho: Imagina una línia recta, i al voltant de cada punt de la línia hi ha una campana de Gauss vertical. Les dades observades estan escampades segons aquestes campanes.
Com trobem els millors β₀ i β₁?
Mètode dels mínims quadrats: minimitzem la suma dels errors al quadrat:
min Σ(yᵢ - (β₀ + β₁xᵢ))²
Per què elevem al quadrat? Per tractar igual errors positius i negatius, i per penalitzar més els errors grans.
Connexió amb probabilitat: Resulta que mínims quadrats és equivalent a màxima versemblança (sota l’assumpció de normalitat)!
Interpretació dels coeficients
β₁ (pendent):
- β₁ = 2000 → “Per cada m² addicional, el preu puja 2000€”
- β₁ = -0.5 → “Per cada hora més de son, la productivitat baixa 0.5 unitats” (relació negativa!)
β₀ (intersecció):
- Sovint no té interpretació pràctica (valor de Y quan X=0 pot ser absurd)
- Però és necessari per fitxar bé la línia
Avaluació del model
R² (R-quadrat): Percentatge de variabilitat de Y explicat per X
- R² = 0: X no explica res de Y
- R² = 1: X explica perfectament Y (tots els punts sobre la línia)
- R² = 0.7: X explica el 70% de la variabilitat de Y
Interpretació pràctica:
- R² < 0.3: Relació dèbil
- R² = 0.5-0.7: Relació moderada
- R² > 0.8: Relació forta
Compte: R² alt no implica causalitat! Només mesura associació.
Inferència sobre els coeficients
No només trobem β₁, també podem:
- Test d’hipòtesi: “És β₁ significativament diferent de 0?” (hi ha relació real?)
- Interval de confiança: “β₁ està probablement entre 1500 i 2500”
Això ens diu si la relació que veiem podria ser casualitat o és real.
Mesurant la incertesa i la informació
Què és l’entropia?
Idea intuïtiva: Mesura com de sorprès estaries de mitjana quan observes el resultat d’una variable aleatòria.
Escenaris extrems:
1. Baixa entropia (poca sorpresa):
- Suposem que tens una moneda molt esbiaixada: 99% cara, 1% creu
- Quasi sempre obtindràs cara → difícilment et sorprèn → entropia baixa
2. Alta entropia (molta sorpresa):
- Suposem que tens una moneda justa: 50% cara, 50% creu
- No pots predir què sortirà → cada llançament pot sorprendre → entropia alta
Fórmula de l’entropia
H(X) = -Σ P(x) × log₂ P(x)
Interpretació: Nombre mitjà de bits necessaris per codificar el resultat.
Exemple numèric simple:
Moneda justa (cara/creu):
H = -[0.5 × log₂(0.5) + 0.5 × log₂(0.5)]
= -[0.5 × (-1) + 0.5 × (-1)]
= 1 bit
Necessites exactament 1 bit per codificar el resultat (0=creu, 1=cara).
Moneda esbiaixada (99% cara, 1% creu):
H = -[0.99 × log₂(0.99) + 0.01 × log₂(0.01)]
≈ 0.08 bits
Necessites molt menys informació perquè gairebé sempre és cara!
Per què importa en ML?
1. Arbres de decisió: L’algorisme escull divisions que maximitzen la reducció d’entropia (guany d’informació).
2. Funció de pèrdua per classificació: L’entropia creuada (següent secció) és la loss function més usada!
Entropia Creuada: Comparant Distribucions
Situació: Tens dues distribucions:
- P: La distribució real (veritat)
- Q: La teva predicció (model)
Entropia creuada H(P, Q): Mesura com de bé Q aproxima P.
H(P, Q) = -Σ P(x) × log Q(x)
Interpretació intuïtiva: Si uses la teva predicció Q per codificar dades que realment venen de P, quants bits necessites?
Propietats:
- H(P, Q) ≥ H(P) (sempre igual o més gran que l’entropia de P)
- H(P, Q) = H(P) només si Q = P (predicció perfecta!)
En ML: Binary Cross-Entropy Loss
Per classificació binària (cat/dog):
Loss = -[y × log(ŷ) + (1-y) × log(1-ŷ)]
On:
- y = label real (0 o 1)
- ŷ = predicció del model (probabilitat entre 0 i 1)
Exemples:
Cas 1: y=1 (realment és “1”), ŷ=0.9 (model prediu 90% “1”)
Loss = -[1 × log(0.9) + 0 × log(0.1)] ≈ 0.105 (pèrdua baixa, bona predicció!)
Cas 2: y=1 (realment és “1”), ŷ=0.1 (model prediu 10% “1”)
Loss = -[1 × log(0.1) + 0 × log(0.9)] ≈ 2.303 (pèrdua alta, mala predicció!)
Per què és bona loss function?
- Penalitza molt les prediccions molt confiades però equivocades
- És derivable (necessari per gradient descent)
- Té interpretació probabilística clara
Divergència de Kullback-Leibler (KL)
Mesura “com de diferent” és Q de P:
KL(P || Q) = Σ P(x) × log(P(x)/Q(x))
Propietats importants:
- KL(P || Q) ≥ 0 sempre
- KL(P || Q) = 0 si i només si P = Q
- NO és simètrica: KL(P || Q) ≠ KL(Q || P)
Relació amb entropia creuada:
KL(P || Q) = H(P, Q) - H(P)
Per tant, minimitzar entropia creuada és equivalent a minimitzar divergència KL!
En ML: Usada en Variational Autoencoders (VAEs), regularització de models, i comparació de distribucions.
Mostreig i Bootstrapping
Per què mostrejar?
Sovint és impossible o massa car observar tota la població:
- Tots els usuaris potencials d’una app (encara no existeixen!)
- Totes les peces que produirà una fàbrica
- Totes les possibles condicions meteorològiques
Solució: Agafem una mostra representativa i fem inferències.
Tipus de mostreig
1. Mostreig aleatori simple:
- Cada element té la mateixa probabilitat de ser escollit
- El més honest però potser no el més eficient
2. Mostreig estratificat:
- Divideixes la població en grups (estrats)
- Mostrejes proporcionalment de cada grup
Exemple: Enquesta d’opinió:
- Estrats: homes/dones, grups d’edat, regions
- Assegures representació de tots els grups
3. Mostreig sistemàtic:
- Esculls cada k-èsim element
- Exemple: Cada 10è client que entra
Risc: Si hi ha patrons periòdics, pots esbiaixar-te!
Bootstrapping: “Crear dades de dades”
El problema: Tens una mostra, però vols saber com de fiable és el teu estimador.
Solució màgica: Remostrejar amb reemplaçament de les teves pròpies dades!
Procediment:
- Tens n dades originals
- Crees una nova mostra de n elements triant aleatòriament amb reemplaçament
- Calcules l’estadístic d’interès (mitjana, mediana, etc.)
- Repeteixes passos 2-3 unes 1000-10000 vegades
- Ara tens una distribució de l’estadístic!
Exemple:
Dades originals: [2, 4, 6, 8, 10]
Mostra bootstrap 1: [2, 2, 6, 8, 10] → mitjana = 5.6 Mostra bootstrap 2: [4, 6, 6, 8, 10] → mitjana = 6.8 Mostra bootstrap 3: [2, 4, 4, 4, 10] → mitjana = 4.8 … (repeteix 10000 vegades)
Ara pots veure la distribució de les mitjanes i calcular intervals de confiança!
Avantatges:
- No necessites assumpcions sobre la distribució
- Funciona amb qualsevol estadístic (fins i tot mediana, quantils…)
- Molt potent i simple
En ML:
- Bagging: Random Forests usen bootstrapping per crear múltiples arbres
- Validació: Estimar incertesa en mètriques del model
- Feature importance: Veure quines variables són estables
Conceptes avançats per a ML
Biaix-Variància Tradeoff: El gran dilema
Descomposició de l’error:
Quan un model fa prediccions, l’error total es pot descompondre en:
Error Total = Biaix² + Variància + Soroll Irreductible
Què és cada part?
1. Biaix (Bias): Error per assumpcions simplificadores
- Alt biaix: El model és massa simple (underfitting)
- Exemple: Usar una línia recta per dades clarament curves
Analogia: Un tirador que sempre dona molt a la dreta (consistent però equivocat).
2. Variància (Variance): Sensibilitat a variacions en les dades
- Alta variància: El model s’adapta massa a les dades específiques (overfitting)
- Exemple: Un polinomi de grau 20 que passa per tots els punts d’entrenament
Analogia: Un tirador que dona per tot arreu (inconsistent).
3. Soroll irreductible: No depèn del model, és inhrent a les dades
El tradeoff:
- Models simples → Alt biaix, Baixa variància
- Models complexos → Baix biaix, Alta variància
L’art del ML: Trobar el punt dolç al mig!
Visualització mental:
Underfitting ←→ Punt òptim ←→ Overfitting
(massa simple) (massa complex)
Com detectar-ho:
- Underfitting: Error alt tant en train com en test
- Overfitting: Error baix en train, error alt en test
- Punt òptim: Error baix en test (similar a train)
Esperança Condicional: la millor predicció possible
Definició: E[Y|X] és el valor esperat de Y per un valor donat de X.
Per què és important:
E[Y|X] és la millor predicció possible de Y donat X (en sentit de mínim error quadràtic).
Traducció: Si poguessis saber la veritable distribució de P(Y|X), la millor predicció seria la mitjana d’aquesta distribució.
Exemple:
Predius salari (Y) a partir d’anys d’experiència (X=5):
- Algunes persones amb X=5 guanyen 30k
- Altres guanyen 35k, 40k, 32k…
- La millor predicció és E[Y|X=5] = mitjana de tots aquests salaris
En ML: El que realment estem intentant aprendre és E[Y|X]! Però només tenim mostres finites, no la distribució completa.
Llei dels grans nombres
Per què més dades és millor?
Enunciat intuïtiu:
Quan augmentes el tamany de la mostra, la mitjana mostral convergeix al valor real.
Visualitza-ho:
- Llances una moneda 10 vegades → Potser obtens 7 cares (70%)
- Llances 100 vegades → Obtens 53 cares (53%)
- Llances 1000 vegades → Obtens 501 cares (50.1%)
- Llances 10000 vegades → Obtens 5003 cares (50.03%)
Com més llances, més proper a la veritat (50%)!
En ML:
- Més dades d’entrenament → millors estimacions dels paràmetres
- Justifica per què datasets grans funcionen millor
- Però compte: necessites dades representatives, no només moltes!
Consells pràctics
Sempre visualitza primer
Abans de fer qualsevol test o model:
- Histogrames per veure distribucions
- Scatter plots per veure relacions
- Box plots per detectar outliers
Per què? Els ulls detecten patrons que els números no mostren.
Comprova assumpcions
Molts mètodes assumeixen:
- Normalitat
- Independència
- Variància constant
No assumeixis, comprova! Tests de normalitat, gràfics residuals, etc.
Correlació ≠ Causalitat
Exemple clàssic: Vendes de gelats i ofegaments estan correlacionats.
Causa real? Ambdós augmenten a l’estiu (variable oculta: temperatura)!
En ML: Els models troben correlacions, no causes. Tingues cura interpretant resultats.
Outliers: No els eliminis automàticament
Poden ser:
- Errors: Typo, sensor defectuós → Elimina’ls
- Dades valuoses: Fraus, esdeveniments rars → Mantén-los!
Investiga abans d’eliminar.
Normalització i Escala
Molts algoritmes (KNN, SVM, xarxes neuronals) són sensibles a l’escala:
Opcions:
- Z-score: (x - μ) / σ → Mitjana 0, desviació 1
- Min-Max: (x - min) / (max - min) → Entre 0 i 1
Quan? Sempre que les features tinguin escales molt diferents.
Validació Creuada: La Teva Millor Amiga
No et refis només de l’error d’entrenament!
- Divideix dades en train/validation/test
- Usa k-fold cross-validation
- Prova en dades que el model no ha vist mai
Simplicitat primer
Principi d’Occam: Entre dos models amb rendiment similar, escull el més simple.
Per què?
- Més interpretable
- Menys propensos a overfitting
- Més fàcil de debugar i mantenir
Comença amb regressió lineal abans de provar xarxes neuronals!
Quantifica la incertesa
No diguis només “la predicció és 42”:
- Dona intervals de confiança
- Mostra probabilitats
- Comunica el risc
Especialment important en aplicacions crítiques (medicina, finances).
Itera i valida constantment
El ML és experimental:
- Prova diferents models
- Ajusta hiperparàmetres
- Valida en dades noves
- Monitora en producció
Documenta les teves assumpcions
Escriu què has assumit:
- Distribució de les dades
- Independència de features
- Estabilitat temporal
Per què? Quan les coses fallen, sabràs on buscar!
Glossari
Variable aleatòria: Resultat numèric d’un procés aleatori
Esperança (E[X]): Valor mitjà esperat
Variància: Mesura de dispersió al voltant de la mitjana
Desviació estàndard: Arrel quadrada de la variància
Correlació: Mesura de relació lineal entre dues variables (-1 a +1)
P-valor: Probabilitat d’observar dades tan extremes si H₀ fos certa
Interval de confiança: Rang on esperem trobar el paràmetre real amb cert nivell de confiança
Biaix: Error sistemàtic del model (underfitting)
Variància: Sensibilitat del model a variacions en les dades (overfitting)
Entropia: Mesura de incertesa o informació
Màxima versemblança: Escollir paràmetres que fan més probables les dades observades
Python bàsic
- Programes
- Execució
- Hola, món
- Estructura i sintaxi
- Tipus de dades
- Cadenes
- Aritmètica i condicions
- Conversions
- Variables
- Mètodes
- Organització del codi
- Funcions
- Control de flux
- Classes i objectes
- Col.leccions
- Definició de classes
- Comparable
- Definició d’iteradors
- Herència
- Classes abstractes
- Gestió d’errors
- Context managers
- Anotacions de tipus
- Python idiomàtic
- Referències
Programes
Els programes consten de mòduls. Els mòduls, al seu torn, poden contenir instruccions, definicions de funcions i/o definicions de classe. Cada mòdul està associat a un fitxer font (extensió .py) i possiblement a un fitxer de bytecode (extensió .pyc).
Un compilador/intèrpret anomenat màquina virtual de Python (PVM) tradueix els fitxers font de Python a bytecode abans de l’execució. La PVM pot desar el bytecode corresponent en fitxers per a execucions posteriors.
Execució
Un programa en Python es pot executar com a script des d’una línia d’ordres de la manera següent:
python helloworld.py
Errors de sintaxi es detecten abans d’executar el programa.
Errors de tipus (TypeError) es detecten en temps d’execució. És millor aclarir-ho així.
Hola, món
La versió més senzilla d’aquest programa consisteix en una sola instrucció:
print("Hola món!")
L’ordre print converteix automàticament les dades en text, les mostra i mou el cursor a la línia següent:
print("Hola món!")
print(34)
print("Hola món!", end = "") # evita el salt de línia
Una altra versió integra aquesta instrucció en una funció principal que es crida al final del mòdul:
def main():
print("Hola món!")
main()
També tenim entrada des del teclat:
nom = input("Introduïu el vostre nom: ") # cadena
edat = int(input("Introduïu la vostra edat: ")) # sencer
Estructura i sintaxi
-
Els literals inclouen números, cadenes, tuples, llistes i diccionaris.
-
Els identificadors inclouen els noms de variables, classes, funcions i mètodes.
-
Les paraules reservades inclouen les de les principals sentències de control (
if,while,for,import, etc.), operadors (in,is, etc.), definicions (def,class, etc.) i valors especials (True,False,None, etc.). -
Els elements lèxics d’una sola línia estan separats per zero o més espais.
-
La sagnia és significativa i s’utilitza per marcar estructures sintàctiques, com ara blocs d’instruccions i codi dins de definicions de funcions, classes i mètodes.
-
Una frase es pot trencar i continuar a la línia següent després d’una coma o mitjançant el símbol ’'.
-
Les capçaleres de les instruccions de control i les definicions de funcions, classes i mètodes acaben amb dos punts (:).
-
Un comentari de final de línia comença amb el símbol #.
# Això és un comentari de final de # línia # (en tres línies).Els docstring, que comencen amb “”“ i acaben amb “”“, són és la forma de documentar funcions, classes o mòduls.
""" Això és una docstring amb diverses línies. """
Tipus de dades
Tots els valors de dades, incloses les funcions, són objectes. I totes les variables són referències a aquests objectes.
intrepresenta nombres sencers.floatrepresenta nombres de coma flotant amb precisió doble.
Cadenes
-
Una cadena és una seqüència de 0 o més caràcters.
-
Les cadenes són instàncies de la classe
str. Les cadenes són objectes immutables. -
La funció
lenretorna el nombre de caràcters d’una cadena. -
L’operador de subíndex ([]) accedeix a un caràcter en una posició determinada.
aString = "Hola món!" print(len(aString), aString[2]) -
Les cadenes es poden comparar utilitzant els operadors de comparació estàndard ==, <, etc.
-
Els literals de cadena es formen utilitzant cometes simples o cometes dobles com a delimitadors.
-
Les cadenes són instàncies de la classe
str. Les cadenes són objectes immutables. -
Els literals de caràcters són simplement cadenes que contenen un sol caràcter.
-
Una seqüència d’escapada es forma utilitzant el caràcter ’' seguit d’una lletra adequada com ara ‘n’ o ‘t’.
L’operador de concatenació + uneix dues cadenes per formar una tercera cadena nova. Els operands d’altres tipus s’han de convertir en cadenes abans de poder ser concatenats.
Exemples:
"35" + " pàgines de llargada."
str(35) + " pàgines de llargada."
# Si x i y són objectes, el codi
str(x) + str(y)
# concatena les seves representacions de cadena
Les f-strings són una manera moderna i elegant de formatar cadenes de text a Python. Permeten incrustar expressions directament dins de literals de cadena de manera més llegible i eficient que els mètodes anteriors:
nom = "Maria"
edat = 25
missatge = f"Hola, em dic {nom} i tinc {edat} anys."
A dins de les claus pot haver-hi expressions.
Si són números, es poden formatar després dels dos punts.
Aritmètica i condicions
Els operadors aritmètics inclouen +, -, *, /, %, // i ** (exponenciació).
L’operació / retorna sempre un float, i l’operació // un sencer, sempre que els operands siguin sencers.
max, min, abs i round són funcions estàndard amb els efectes estàndard (round retorna un nombre sencer).
El mòdul matemàtic inclou funcions estàndard per a trigonometria, logaritmes, arrels quadrades, etc.
Exemples:
round(3.14)
math.sqrt(2)
Els operadors de comparació són ==, !=, <, >, <= i >=.
Tots els operadors de comparació retornen True o False.
Només els objectes que són comparables, com ara nombres i seqüències d’objectes comparables (cadenes i llistes de nombres o cadenes), poden ser operands per a comparacions.
Exemple:
print("AAB" > "AAA")
El tipus booleà bool inclou els valors constants True i False (tingueu en compte les majúscules).
Altres valors, com ara 0, ‘’, [] i None, també signifiquen False. Pràcticament qualsevol altre valor també significa True.
Els operadors lògics són not, and i or.
Les expressions booleanes compostes consten d’un o més operands booleans i un operador lògic. L’avaluació en curtcircuit s’atura quan hi ha prou informació disponible per retornar un valor. not s’avalua abans de and, que s’avalua abans d’or.
Conversions
Els tipus numèrics es poden convertir a altres tipus numèrics mitjançant les funcions de conversió de tipus adequades. El nom de la funció és el nom del tipus de destinació.
Exemples:
int(3.14)
float(3)
Les funcions ord i chr s’utilitzen per convertir entre sencers i caràcters, de la manera següent:
ord('A')
chr(65)
La funció str converteix qualsevol objecte Python a la seva representació de cadena corresponent.
str(45)
str(3.14)
Variables
Forma:
<variable> = <expressió>
Exemples:
x = 1
x = x + 3.14
- Una variable s’introdueix i s’estableix a un valor inicial mitjançant una instrucció d’assignació.
- Qualsevol variable pot anomenar qualsevol objecte i es pot reiniciar a qualsevol objecte.
- Una variable selecciona el tipus de l’objecte al qual està actualment vinculada.
- La comprovació de tipus i la comprovació de referències a variables no inicialitzades es realitzen en temps d’execució.
Les assignacions tenen la forma:
<variable> = <expressió>
Exemple:
x = 1
x = x + 3.14
Les variables en si mateixes no tenen tipus. Qualsevol variable pot anomenar qualsevol cosa i reiniciar-se a qualsevol cosa. L’objecte al qual fa referència una variable té un tipus.
Mètodes
Una crida a un mètode d’instància consisteix en una referència d’objecte (també anomenada objecte receptor), seguida d’un punt, seguit del nom del mètode i una llista d’arguments entre parèntesis.
Exemple:
aList.sort()
Una crida a un mètode de classe consisteix en un nom de classe, seguit d’un punt, seguit del nom del mètode i una llista d’arguments entre parèntesis.
Un mètode retorna el tipus de valor indicat per la seva instrucció return, si n’hi ha. Si un mètode no retorna explícitament un valor, per defecte es retorna el valor None. Les compatibilitats de tipus es resolen en temps d’execució.
Organització del codi
Els mòduls de Python s’utitzen per defecte en la forma de singletons, com els mòduls de JavaScript. Comparativament a llenguatges com Java, les classes només s’utilitzen quan cal estat individual.
Un exemple de singleton:
value = 0
def increment():
global value
value += 1
return value
Mòduls
Un mòdul és un fitxer .py amb definicions (funcions, classes, constants…). S’importa mitjançant la forma:
import <nom del mòdul>
Els recursos d’aquest mòdul es referencian mitjançant la forma
<nom del mòdul>.<nom del recurs>
Exemple:
import math
print(math.sqrt(2), math.pi)
Alternativament, es pot importar un recurs individual mitjançant la forma
from <nom del mòdul> import <nom del recurs>
Aleshores es referencia el recurs sense el nom del mòdul com a qualificador.
Exemples:
from math import sqrt, pi
print(sqrt(2), pi)
from random import *
# Referencia tots els recursos des de random: NO RECOMMANABLE
Es pot assignar un àlies al mòdul o recurs per escurçar-lo o evitar conflictes:
from datetime import datetime as dt
Paquets
Un paquet és un directori que conté un arxiu especial __init__.py i altres mòduls. Això permet agrupar diversos mòduls sota un mateix “nom d’espai”.
mypackage/
__init__.py
utils.py
models.py
Es pot importar amb:
import mypackage.utils
from mypackage import models
Funcions
A diferència dels mètodes, que són funcions associades a un objecte o una classe, Python disposa de funcions independents o funcions globals que no pertanyen a cap objecte i que es poden cridar directament.
Aquestes funcions es poden definir en un mòdul o directament en un script o a l’intèrpret.
Exemple:
def suma(a, b):
return a + b
resultat = suma(3, 4)
print(resultat)
Python inclou moltes funcions built-in (integrades) que es poden utilitzar sense importar cap mòdul. Aquestes funcions permeten fer operacions comunes de manera senzilla i eficient.
Algunes funcions built-in importants:
| Funció | Descripció | Exemple |
|---|---|---|
print | Escriu dades a la sortida estàndard | print("Hola") |
len | Retorna la longitud d’una col·lecció o cadena | len("abc") retorna 3 |
type | Retorna el tipus d’un objecte | type(3) retorna <class 'int'> |
int | Converteix un valor a enter | int("10") retorna 10 |
str | Converteix un valor a cadena | str(5) retorna "5" |
range | Genera una seqüència d’enters | list(range(3)) retorna [0,1,2] |
input | Llegeix entrada des del teclat | input("Escriu algo: ") |
Àmbit de les variables
-
global: fa que una variable dins d’una funció faci referència a la definida al nivell global del mòdul. Sense global, una assignació dins d’una funció crea una variable local. -
nonlocal: fa que una variable dins d’una funció interna (nested function) faci referència a la definida a la funció externa, no a l’àmbit global.
Python resol els noms seguint l’ordre “LEGB”:
- Local (dins de la funció actual)
- Enclosing (funció externa si hi ha funcions niades)
- Global (del mòdul actual)
- Built-in (funcions i noms predefinits de Python, com len, print)
Arguments per defecte i nominals
En Python, els paràmetres d’una funció poden tenir valors per defecte, que s’utilitzen quan la crida no proporciona un valor per a aquest paràmetre.
def saluda(nom="Món"):
print(f"Hola, {nom}!")
saluda() # Hola, Món!
saluda("Julián") # Hola, Julián!
Els arguments també es poden passar per nom, independentment de l’ordre:
def mostra_info(nom, edat):
print(f"Nom: {nom}, Edat: {edat}")
mostra_info(edat=25, nom="Anna")
Funcions com a objectes
Les funcions a Python són objectes de primera classe, això significa que es poden:
- Assignar a variables
- Passar com a arguments a altres funcions
- Retornar des de funcions
def quadrat(x):
return x * x
f = quadrat
print(f(5)) # 25
def aplica_funcio(f, valor):
return f(valor)
print(aplica_funcio(quadrat, 7)) # 49
Les funcions lambda són funcions anònimes i petites, definides en una sola línia, útils quan es necessita una funció senzilla sense crear-ne una amb def.
suma = lambda a, b: a + b
print(suma(3, 4)) # 7
# Usos habituals en funcions com map, filter, sorted
llista = [1, 4, 2, 5]
ordenada = sorted(llista, key=lambda x: -x) # ordre descendent
print(ordenada) # [5, 4, 2, 1]
Es poden definir funcions dins d’altres funcions. Aquestes funcions internes poden accedir a variables del seu entorn exterior. Quan una funció interna recorda l’estat de les variables externes encara que la funció exterior hagi acabat, es diu que és un closure.
Exemple:
def multiplicador(n):
def multiplica(x):
return x * n
return multiplica
per_3 = multiplicador(3)
print(per_3(10)) # 30
Control de flux
while
Té la forma:
while <expressió booleana>:
<instrucció>
<instrucció>
Les instruccions del cos del bucle estan marcades amb sagnat.
L’instrucció break surt d’un bucle:
while True:
break
L’instrucció return surt d’una funció o mètode. Si no s’especifica cap expressió, es retorna el valor None:
return 0
L’instrucció pass no fa res:
while True:
pass
for
Només hi ha un tipus de bucle for, que visita cada element d’un objecte iterable, com ara una cadena o una llista.
Forma:
for <variable> in <iterable>:
<instrucció>
…
<instrucció>
Exemple:
for s in aListOfStrings:
print(s)
La variable recull el valor de cada element de l’objecte iterable i és visible al cos del bucle.
Les instruccions del cos del bucle estan marcades amb sagnat.
Els bucles simples controlats per recompte que iteren a través d’un rang d’enters tenen tres formes:
for <variable> in range(<límit superior>):
<instrucció>
…
<instrucció>
for <variable> in range(<límit inferior>, <límit superior>):
<instrucció>
…
<instrucció>
for <variable> in range(<límit inferior>, <límit superior>, <valor del pas>):
<instrucció>
…
<instrucció>
if
Forma:
if <expressió booleana>:
<instrucció>
…
<instrucció>
elif <expressió booleana>:
<instrucció>
…
<instrucció>
else:
<instrucció>
…
<instrucció>
Les sentències del conseqüent i de cada alternativa estan marcades amb sagnat.
Classes i objectes
La forma d’instanciar objectes és:
<nom de la classe>(<arguments>)
Exemple:
compte = CompteCorrent("Ken", "1111", 500.00)
Les variables i els paràmetres en si mateixos no tenen tipus. Qualsevol variable o paràmetre pot anomenar qualsevol cosa i reiniciar-se a qualsevol cosa. L’objecte al qual fa referència una variable o paràmetre té un tipus.
Col.leccions
| Tipus | Mutabilitat | Ordenat | Permet duplicats |
|---|---|---|---|
list | Sí | Sí | Sí |
tuple | No | Sí | Sí |
set | Sí | No | No |
frozenset | No | No | No |
dict | Sí | Sí (>=3.7) | No (claus) |
Llistes
-
Una llista és una seqüència mutable de 0 o més objectes de qualsevol tipus.
-
Les llistes tenen una representació literal, de la forma:
[e0, e1, …, en-1] -
La funció
lenretorna el nombre d’elements d’una llista. -
L’operador de subíndex ([]) accedeix a un element en una posició determinada.
Exemple:
aList = [45, 56, 67]
print(len(aList), aList[2])
- Les llistes d’objectes comparables es poden comparar utilitzant els operadors de comparació estàndard ==, <, etc.
- La classe list inclou molts mètodes útils per a insercions, eliminacions, cerques, etc.
Sets
- Un
setés una col·lecció mutable de 0 o més objectes únics de qualsevol tipus. - La funció
lenretorna el nombre d’elements d’un conjunt. - La classe
setinclou molts mètodes útils per a insercions, eliminacions, cerques, etc.
set1 = set()
for x in range(10):
set1.add(x)
for x in set1: print(x)
set2 = set([1,2,3])
set3 = set1.intersection(set2)
Diccionaris o mapes
- Un diccionari és una col·lecció mutable de 0 o més parells clau/valor únics. Dit d’una altra manera, un diccionari conté un conjunt de claus, on cada clau està associada a un valor.
- La funció len retorna el nombre d’elements d’un diccionari.
- L’operador de subíndex accedeix a un valor en una clau existent. Aquest operador també es pot utilitzar per afegir una clau nova o per substituir un valor en una clau existent.
- La classe dict inclou molts mètodes útils. Exemple:
# Associa 10 edats aleatòries entre 1 i 10 amb noms consecutius
names = {}
for i in range(1, 11):
name = "Nom" + str(i)
names[name] = random.randint(1, 10)
# Imprimeix totes les claus i els seus valors
for key in names.keys():
print(key, names[key])
Tuples
- Una
tupleés una seqüència immutable de 0 o més objectes de qualsevol tipus. - Les tuples són útils per agrupar valors que no han de canviar.
- Es poden accedir per índex i utilitzar en bucles igual que les llistes.
- Es poden usar com a claus de diccionaris, a diferència de les llistes (ja que són immutables).
tup1 = () # tupla buida
tup2 = (1, 2, 3) # tres enters
tup3 = ("a", 3.14, True) # valors heterogenis
print(tup2[0]) # accés per índex
for item in tup3: print(item)
if 2 in tup2:
print("2 està a tup2")
Iteradors
Un iterador és un objecte que admet el recorregut d’una col·lecció. La PVM utilitza automàticament un iterador sempre que veu un bucle for.
La funció iter espera una col·lecció iterable com a argument i retorna un objecte iterador per a aquesta col·lecció.
Example:
i = iter([1, 2, 3])
Un iterador registra un punter de posició actual a un element de la col·lecció. El mètode iterador next(i) avança aquest punter i retorna l’element visitat més recentment.
Example:
print (next(i), next(i), next(i))
Quan next ha retornat l’últim element de la col·lecció, qualsevol crida posterior de next generarà una excepció StopIteration. Quan el programador hagi acabat d’utilitzar un iterador, s’ha de tancar mitjançant el mètode close().
Per recórrer tots els elements amb un iterador, incrusteu la crida de next en una instrucció try/except, de la manera següent:
while True:
try:
element = next(i)
except StopIteration:
break
Comprehensions
Les comprehensions són formes compactes i llegibles de construir col·leccions a partir d’iterables. Substitueixen bucles for simples i són molt utilitzades en codi Python idiomàtic.
Les llistes es fan amb [expressió for element in iterable if condició] (l’if és opcional).
# Nombres parells entre 0 i 9
pares = [x for x in range(10) if x % 2 == 0]
# Quadrats dels nombres
quadrats = [x**2 for x in range(5)]
Els conjunts tenen la mateixa forma però amb {}:
# Lletres úniques en una cadena
vocals = {c for c in 'informàtica' if c in 'aeiou'}
Els diccionaris es fan amb {clau: valor for element in iterable if condició}:
# Taula de quadrats
taula = {x: x**2 for x in range(5)}
# Nom i llargada
noms = ["Anna", "Joan", "Pau"]
longituds = {nom: len(nom) for nom in noms}
Generadors
Un generador és una manera fàcil de crear iteradors personalitzats. En lloc d’implementar una classe amb els mètodes __iter__ i __next__, podem usar una funció que utilitza la instrucció yield per produir valors un a un.
Quan s’executa una funció generadora, no es retorna un valor immediatament, sinó un objecte generador que implementa el protocol d’iterador.
def comptar_fins(n):
i = 1
while i <= n:
yield i
i += 1
gen = comptar_fins(3)
print(next(gen)) # 1
print(next(gen)) # 2
print(next(gen)) # 3
Quan yield s’executa, la funció es pausa i el valor es retorna. L’execució es reprèn en el punt on es va deixar, mantenint l’estat local (com ara el valor de i).
També es poden utilitzar els generadors directament en bucles:
for x in comptar_fins(3):
print(x)
Definició de classes
Les definicions de classes tenen la forma general:
class <name>(<superclass name>):
<class variables>
<class methods>
<instance methods>
La superclasse en parèntesis s’omet per a classes bàsiques. Els ítems dins de la definició de la classe poden aparèixer en qualsevol ordre.
Example:
class Student:
NUM_GRADES = 5
def __init__(self, name):
self.name = name
self.grades = []
for i in range(Student.NUM_GRADES):
self.grades.append(0)
def getName(self): return self.name
def getGrade(self, i):
return self.grades[i - 1]
def setGrade(self, i, newGrade):
self.grades[i - 1] = newGrade
def __str__(self):
"""Format: Name on the first line
and all grades on the second line,
separated by spaces.
"""
result = self.name + '\n'
result += ''.join(map(str, self.grades))
return result
Ús:
s = Student('Mary')
for i in range(1, Student.NUM_GRADES + 1)
s.setGrade(i, 100)
print(s)
Visibilitat
Tots els elements definits dins d’una classe (variables o mètodes) són potencialment visibles per als programadors que utilitzen la classe.
Els implementadors d’una classe solen desaconsellar l’accés directe a les variables utilitzant el subratllat als seus noms.
Variables i constructors
Les variables d’instància sempre tenen com a prefix la paraula reservada self. Normalment s’introdueixen i s’inicialitzen en un mètode constructor anomenat __init__.
En l’exemple següent, les variables self.name i self.grades són variables d’instància, mentre que la variable NUM_GRADES és una variable de classe:
class Student:
NUM_GRADES = 5
def __init__(self, name):
self.name = name
self.grades = []
for i in range(Student.NUM_GRADES):
self.grades.append(0)
La PVM crida automàticament el mètode constructor quan el programador sol·licita una nova instància de la classe, de la següent manera:
s = Student('Mary')
El mètode constructor sempre espera almenys un argument, self. Quan es crida el mètode, l’objecte que s’està instanciant es passa aquí i, per tant, està vinculat a self al llarg del codi. Es poden donar altres arguments per proporcionar valors inicials per a les dades de l’objecte.
Un constructor per defecte no espera cap argument de la crida i assigna valors per defecte raonables a les variables d’instància d’un objecte. Altres constructors esperen un o més arguments que permetin al programador especificar aquests valors.
Com que no hi ha sobrecàrrega de mètodes, només hi pot haver un mètode __init__. Tanmateix, es poden emular diversos mètodes mitjançant paràmetres opcionals amb valors per defecte.
Per exemple, el mètode __init__ de la classe següent definirà el nom de l’estudiant com una cadena buida si la crida no proporciona cap nom:
class Student:
NUM_GRADES = 5
def __init__(self, name = ''):
self.name = name
self.grades = []
for i in range(Student.NUM_GRADES):
self.grades.append(0)
Ús:
s1 = Student('Mary')
S2 = Student()
Mètodes d’instància
La forma d’una definició de mètode d’instància és
def <nom>(self, <altres arguments>):
<instruccions>
Una definició de mètode no té un tipus de retorn explícit. Si un mètode no retorna un valor d’una instrucció return, retorna automàticament el valor None. En cas contrari, el mètode retorna el tipus de valor retornat per una instrucció return o el valor None si no hi ha aquest valor.
L’argument self és necessari per a un mètode d’instància. Quan es crida el mètode, el mètode cridat no passa explícitament un argument a la posició de self. En canvi, la PVM assigna a self l’objecte receptor.
Quan es crida un mètode, els seus arguments han de coincidir en nombre amb els paràmetres corresponents de la definició. Com de costum, la comprovació de tipus es difereix fins al punt en què es necessita un valor per a una operació específica del tipus.
Exemple:
class Student:
# Variables and constructor
def getName(self): return self.name
def getGrade(self, i):
return self.grades[i - 1]
def setGrade(self, i, newGrade):
self.grades[i - 1] = newGrade
La sobrecàrrega de mètodes s’aconsegueix mitjançant paràmetres opcionals i predeterminats o comprovant el tipus dels paràmetres i responent-hi adequadament.
Per exemple, una classe Student pot tenir tres mètodes anomenats resetGrades per restablir totes les qualificacions.
El primer mètode, que no espera cap argument, restableix cada qualificació a 0.
El segon mètode, que espera un únic argument enter, restableix cada qualificació a aquest enter.
El tercer mètode, que espera una llista d’enters com a argument, restableix les qualificacions a aquests enters a les posicions corresponents de la llista.
Per aconseguir-ho, es defineix un únic mètode amb un únic paràmetre opcional. Quan es crida el mètode, el valor d’aquest paràmetre serà 0 (no s’ha donat cap argument), un enter (qualsevol nova qualificació per assignar totes les qualificacions) o una llista d’enters. Si el tipus del paràmetre és una llista, el mètode utilitza un índex per obtenir la qualificació de la llista. En cas contrari, el mètode utilitza el valor directament.
def resetGrades(self, value = 0):
for i in range(Student.NUM_GRADES):
if type(value) == list:
self.grades[i] = value[i]
else:
self.grades[i] = value
Ús:
s = Student();
s.resetGrades(100)
s.resetGrades()
newGrades = [85, 66, 90, 100, 73]
s.resetGrades(newGrades)
Mètodes i variables de classe
- Les variables de classe anomenen dades que comparteixen totes les instàncies d’una classe.
- Una variable de classe s’introdueix mitjançant una simple instrucció d’assignació dins d’una classe.
- Les referències a variables de classe sempre tenen com a prefix el nom de la classe.
- Les variables de classe s’escriuen en majúscules per convenció.
Exemple:
class Student:
NUM_GRADES = 5
def __init__(self, name):
self.name = name
self.grades = []
for i in range(Student.NUM_GRADES):
self.grades.append(0)
Altres usos:
print(Student.NUM_GRADES)
Els mètodes de classe són mètodes que no saben res sobre les instàncies d’una classe, però poden accedir a variables de classe i cridar altres mètodes de classe per a diversos propòsits. Per exemple, un mètode per convertir una nota numèrica en una nota amb lletra es pot definir com a mètode de classe a la classe Student.
Les crides a mètodes de classe sempre tenen com a prefix el nom de la classe.
Exemple:
class Student:
# Instance method definitions
@classmethod
def getLetterGrade(cls, grade):
if grade > 89:
return 'A'
elif grade > 79:
return 'B'
else:
return 'F'
Ús:
s = Student()
for i in range(1, Student.NUM_GRADES + 1):
print(Student.getLetterGrade(s.getGrade(i)))
Totes les variables es poden reiniciar en qualsevol moment. No hi ha constants simbòliques. Els símbols com ara True, False i None són paraules reservades.
La funció str converteix qualsevol objecte a la seva representació de cadena.
Exemple:
str(3.14) # retorna '3.14'
Aquesta funció es pot personalitzar per retornar la cadena adequada per a objectes de qualsevol classe definida pel programador incloent-hi un mètode __str__.
Quan el mètode __str__ està disponible, operacions com ara imprimir l’utilitzen automàticament per obtenir la representació de cadena d’un objecte.
Exemple:
class Student:
NUM_GRADES = 5
def __init__(self, name):
self.name = name
self.grades = []
for i in range(Student.NUM_GRADES):
self.grades.append(0)
def __str__(self):
"""Format: Name on the first line
and all grades on the second line,
separated by spaces.
"""
result = self.name + '\n'
result += ''.join(map(str, self.grades))
return result
Igualtat
L’operador == compara dos objectes per verificar la igualtat. Aquest operador utilitza l’operador is per defecte. L’operador is comprova la igualtat de dues referències d’objectes: es refereixen exactament al mateix objecte a la memòria? Sovint, aquesta prova és massa restrictiva. Una versió més relaxada compararia un o més dels atributs dels objectes per verificar la igualtat. Per exemple, dos objectes Student podrien tenir el mateix nom i considerar-se iguals, tot i que també són objectes diferents. Aquest tipus d’igualtat s’anomena equivalència estructural, a diferència del tipus més restrictiu d’identitat d’objecte.
El programador pot anul·lar la definició per defecte de == incloent una definició del mètode __eq__ en una classe determinada. En realitat, hi ha tres proves a realitzar. L’objecte receptor (self) i el segon objecte de paràmetre (other) es comparen primer per la identitat mitjançant l’operador is. Si aquesta prova falla, es comparen els tipus dels dos objectes. Si aquesta prova té èxit, els seus atributs rellevants es comparen mitjançant l’operador ==.
El comportament de != també es pot modificar anul·lant el mètode __ne__.
Exemple:
class Student:
NUM_GRADES = 5
def __init__(self, name):
self.name = name
self.grades = []
for i in range(Student.NUM_GRADES):
self.grades.append(0)
def __eq__(self, other):
"""
Compare the names for equality if the
objects are not identical.
"""
if self is other:
return True
elif type(self) != type(other):
return False
else:
return self.name == other.name
def __ne__(self, other):
"""
Compare the names for inequality.
"""
return not self == other
Usage:
s1 = Student('Mary')
s2 = Student('Bill')
s3 = Student('Bill')
print(s1 == s2) # displays False
print(s2 == s3) # displays True
print(s2 is s3) # displays False
print(s2 != s3) # displays False
Comparable
Els objectes comparables reconeixen els operadors de comparació ==, !=, <, >, <= i >=. Les cadenes són comparables. La classe str inclou els mètodes __eq__ i __lt__. Utilitzant aquests dos mètodes, Python genera automàticament el codi per a les comparacions adequades quan troba qualsevol dels altres operadors de comparació que s’utilitzen amb cadenes.
Es pot ordenar una llista de cadenes mitjançant el mètode sort(), però no es pot ordenar una llista d’objectes Student. Tanmateix, el programador pot incloure els mètodes __eq__ i __lt__ a la classe Student per resoldre aquest problema. Cada mètode utilitza els noms dels estudiants com a atributs comparables.
Tingueu en compte que els conceptes de menor que i major que són més restrictius que la igualtat. És a dir, ara se suposa que els tipus d’objectes que es comparen són els mateixos, de manera que qualsevol error es generarà en temps d’execució.
class Student:
NUM_GRADES = 5
def __init__(self, name):
self.name = name
self.grades = []
for i in range(Student.NUM_GRADES):
self.grades.append(0)
def __eq__(self, other):
if self is other:
return True
elif type(self) != type(other):
return False
else:
return self.name == other.name
def __lt__(self, other):
return self.name < other.name
Usage:
s1 = Student('Mary')
s2 = Student('Bill')
print(s1 < s2) # displays False
print(s1 > s2) # displays True
Classes internes
Una classe es pot definir només per al seu ús en una altra definició de classe. Per exemple, una classe LinkedStack podria utilitzar una classe OneWayNode. Idealment, la definició de OneWayNode s’inclouria dins de LinkedStack, però això no està permès. A tall de comparació, aquí teniu una implementació d’aquestes dues classes:
class OneWayNode:
def __init__(self, data, next):
self.data = data
self.next = next
class LinkedStack:
def __init__(self):
self.items = None
self.size = 0
def push(self, element):
self.items = OneWayNode(element, self.items)
self.size += 1
def pop(self):
element = self.items.data
self.items = self.items.next
self.size -= 1
return element
def peek(self):
return self.items.data
def isEmpty(self):
return len(self) == 0
def __len__(self):
return self.size
Definició d’iteradors
Per convenció, el mètode iter() retorna un iterador sobre un objecte iterable. L’usuari d’un iterador pot esperar que el mètode next() retorni el següent objecte d’una iteració, fins que next genera una excepció StopIteration. En aquest punt, es tanca l’iterador mitjançant el mètode close().
Suposem que la classe LinkedStack ara inclou un mètode iter. Aleshores, es poden visitar els objectes d’una pila, de dalt a baix, de qualsevol de les maneres següents:
stack = LinkedStack()
i = stack.iter()
while True:
try:
element = next(i)
# process element
except StopIteration:
break
for element in stack:
# process element
El mètode iter crea i retorna un objecte generador. El codi d’aquest generador s’executa en un procés independent, de manera simultània amb el procés que utilitza l’iterador. Un generador pot mantenir l’estat intern, com per exemple un punter que indica la posició actual dins dels elements de la col·lecció. En l’exemple actual, aquesta referència apunta inicialment al primer node de la llista enllaçada de la pila.
El codi del generador conté un bucle while True. Si la posició actual és None, vol dir que s’ha arribat a l’últim node, i el generador llança una excepció StopIteration. En cas contrari, retorna l’element del node actual. La instrucció yield pausa l’execució del generador fins que es crida el mètode next(), que retorna l’element generat. Quan el control torna al generador, el punter s’actualitza al següent node. Aquest procés pot continuar indefinidament, excepte si l’usuari crida el mètode close() del generador.
Exemple:
class OneWayNode:
def __init__(self, data, next):
self.data = data
self.next = next
class LinkedStack:
def __init__(self):
self.items = None
self.size = 0
def push(self, element):
self.items = OneWayNode(element, self.items)
self.size += 1
def pop(self):
element = self.items.data
self.items = self.items.next
self.size -= 1
return element
def peek(self):
return self.items.data
def isEmpty(self):
return len(self) == 0
def __len__(self):
return self.size
def __iter__(self):
curPos = self.items
while True:
if curPos is None:
raise StopIteration
yield curPos.data
curPos = curPos.next
Herència
En un llenguatge de programació orientat a objectes, es pot definir una nova classe que reutilitza el codi d’una altra classe. La nova classe esdevé una subclasse de la classe existent, que també s’anomena classe pare. La subclasse hereta tots els atributs, inclosos els mètodes i les variables, de la seva classe pare i de qualsevol classe ancestral de la jerarquia.
La subclassificació és una manera convenient de proporcionar funcionalitats addicionals o més especialitzades a un recurs existent. Per exemple, una cua garanteix l’accés als seus elements en ordre de primer a entrar, primer a sortir. Una cua de prioritat es comporta en la majoria dels aspectes igual que una cua, excepte que els elements de més prioritat s’eliminen primer. Si els elements tenen la mateixa prioritat, s’eliminen en ordre FIFO estàndard. Una cua de prioritat és, doncs, una subclasse de cua, amb un mètode especialitzat per a insercions que garanteix l’ordre adequat dels elements. La cua de prioritat obté tots els seus altres mètodes de la classe de cua gratuïtament.
Aquí teniu la definició d’una classe LinkedQueue per a cues ordinàries:
class OneWayNode:
def __init__(self, data, next):
self.data = data
self.next = next
class LinkedQueue:
def __init__(self):
self.front = self.rear = None
self.size = 0
def enqueue(self, element):
n = OneWayNode(element, None)
if self.isEmpty():
self.rear = self.front = n
else:
self.rear.next = n
self.rear = n
self.size += 1
def dequeue(self):
element = self.front.data
self.front = self.front.next
self.size -= 1
if self.isEmpty():
self.rear = None
return element
def peek(self):
return self.front.data
def isEmpty(self):
return len(self) == 0
def __len__(self):
return self.size
La forma per definir una subclasse és:
class <nom de la subclasse>(<nom de la classe pare>):
<variables i mètodes>
La classe LinkedPriorityQueue és una subclasse de LinkedQueue. La nova classe inclou només dos mètodes, __init__ i enqueue. El mètode __init__ de LinkedPriorityQueue simplement crida el mateix mètode a la seva classe pare, utilitzant la forma:
<nom de la classe pare>.__init__(self)
Això fa que la classe pare inicialitzi les seves variables d’instància.
El nou mètode enqueue primer comprova si hi ha una cua buida i, si això és cert, crida el mètode enqueue a la classe pare utilitzant la forma:
<nom de la classe pare>.<nom del mètode>(<arguments>)
En cas contrari, el mètode busca la posició adequada del nou element i l’insereix allà. Com que la part frontal de la cua es troba al capdavant de l’estructura enllaçada, la cerca s’atura quan l’element entrant és estrictament menor que l’element de la cua. Així, un element entrant es col·loca darrere de tots els elements de la mateixa prioritat.
Here is the definition of LinkedPriorityQueue:
class LinkedPriorityQueue(LinkedQueue):
def __init__(self):
LinkedQueue.__init__(self)
def enqueue(self, element):
if self.isEmpty():
LinkedQueue.enqueue(self, element)
else:
probe = self.front
while probe != None and element >= probe.data:
trailer = probe
probe = probe.next
if probe == None: # At rear of queue
self.rear.next = OneWayNode(element, None)
self.rear = self.rear.next
elif probe == self.front: # At front of queue
self.front = Node(element, self.front.next)
else: # Betwixt two nodes
trailer.next = Node(element, probe)
self.size += 1
Classes abstractes
Quan dues o més classes contenen codi redundant, es pot factoritzar en una classe pare comuna. Aquesta classe normalment s’anomena classe abstracta, perquè no està instanciada.
Considereu dues implementacions de piles, ArrayStack i LinkedStack. Els seus contenidors interns d’elements són diferents, així com els seus mètodes per afegir, eliminar i iterar a través d’aquestes estructures. Tanmateix, si suposem que cada pila fa un seguiment de la seva mida de la mateixa manera, els mètodes len i isEmpty seran els mateixos per a qualsevol implementació. I si suposem que cada implementació inclou un mètode iter, podem implementar diversos altres mètodes, com ara str i addAll, només una vegada per a totes les piles.
L’exemple següent defineix una classe AbstractStack que inclou la informació comuna a totes les implementacions de pila. Per accedir a aquests recursos, una implementació de pila, com ara ArrayStack o LinkedStack, simplement estén AbstractStack.
class AbstractStack:
def __init__(self):
self.size = 0
def isEmpty(self):
return len(self) == 0
def __len__(self):
return self.size
def __str__(self):
result = ''
for element in self:
result += str(element) + '\n'
return result
def addAll(self, otherIterable):
for element in otherIterable:
self.push(element)
AbstractStack és responsable d’inicialitzar només una variable d’instància, size. Cal tenir en compte que el mètode str utilitza un bucle for sobre self, cosa que implica que l’objecte de pila és iterable. En conseqüència, cada implementació de pila ha d’incloure la seva pròpia definició del mètode iter. Finalment, cal tenir en compte que el mètode addAll copia elements d’un altre objecte interable a la pila, utilitzant el mètode push. Per tant, el mètode push també s’ha d’incloure a cada implementació de pila.
La primera implementació, ArrayStack, inclou un mètode init que permet al programador especificar un objecte iterable opcional com a argument. Si aquest argument no és None, es passa al mètode addAll per copiar els seus elements a la nova pila. També es pot cridar addAll per afegir diversos elements en qualsevol moment posterior. ArrayStack també és responsable de mantenir la seqüència d’elements. Per tant, inclou les definicions de peek, pop, push i iter.
Exemple:
class ArrayStack(AbstractStack):
def __init__(self, otherIterable = None):
AbstractStack.__init__(self)
self.items = []
if otherIterable != None:
self.addAll(otherIterable)
def push(self, element):
self.items.append(element)
self.size += 1
def pop(self):
self.size -= 1
return self.items.pop()
def peek(self):
return self.items[-1]
def __iter__(self):
probe = len(self.items) - 1
while True:
if probe < 0:
raise StopIteration
yield self.items[probe]
probe -= 1
Els mateixos mètodes es defineixen a LinkedStack, però és clar que accedeixen a elements en un tipus d’estructura interna molt diferent.
class OneWayNode:
def __init__(self, data, next):
self.data = data
self.next = next
class LinkedStack(AbstractStack):
def __init__(self, otherIterable = None):
AbstractStack.__init__(self)
self.items = None
if otherIterable != None:
self.addAll(otherIterable)
def push(self, element):
self.items = OneWayNode(element, self.items)
self.size += 1
def pop(self):
element = self.items.data
self.items = self.items.next
self.size -= 1
return element
def peek(self):
return self.items.data
def __iter__(self):
probe = self.items
while True:
if probe is None:
raise StopIteration
yield probe.data
probe = probe.next
Gestió d’errors
Python permet capturar i gestionar errors mitjançant blocs try i except. Això evita que el programa s’aturi de manera inesperada i permet reaccionar davant d’errors de manera controlada.
try:
# codi que pot provocar una excepció
x = int(input("Introdueix un nombre: "))
resultat = 10 / x
except ValueError:
print("No has introduït un nombre enter.")
except ZeroDivisionError:
print("No pots dividir per zero.")
finally:
print("Això s'executa sempre, amb o sense error.")
try: conté el codi que pot generar una excepció.except: captura un error específic i executa el codi corresponent.finally: (opcional) conté codi que s’executa sempre, hagi passat o no una excepció. Es poden afegir múltiples blocsexceptper tractar diferents tipus d’errors.- Es pot capturar l’excepció en una variable amb
as:
try:
1 / 0
except ZeroDivisionError as e:
print(f"S'ha produït un error: {e}")
Es poden provocar errors manualment mitjançant raise, per exemple per indicar que alguna condició no es compleix:
def divideix(a, b):
if b == 0:
raise ValueError("No es pot dividir per zero.")
return a / b
Tipus d’excepcions comunes:
| Excepció | Quan apareix |
|---|---|
ValueError | Conversió incorrecta de tipus (int("abc")) |
ZeroDivisionError | Divisió entre zero |
TypeError | Operacions entre tipus incompatibles |
IndexError | Índex fora de rang en una seqüència |
KeyError | Clau inexistent en un diccionari |
FileNotFoundError | Arxiu inexistent |
Context managers
Un gestor de context és un objecte que defineix accions a fer abans i després d’un bloc de codi. S’utilitzen habitualment per gestionar recursos que s’han d’obrir i tancar, com ara fitxers, connexions de xarxa o bases de dades.
La forma més comuna d’utilitzar-los és mitjançant la instrucció with.
with open("notes.txt", "r") as fitxer:
contingut = fitxer.read()
open(...)obre el fitxer i retorna un objecte.- El bloc dins de
withllegeix el contingut. - Quan acaba el bloc, el fitxer es tanca automàticament, fins i tot si hi ha un error.
- Això evita haver d’escriure
fitxer.close()manualment i millora la seguretat i neteja del codi.
Podem definir els nostres propis gestors de context mitjançant una classe amb dos mètodes especials:
__enter__: s’executa al començar el bloc.__exit__: s’executa al final, fins i tot si hi ha excepcions.
Anotacions de tipus
Python permet indicar els tipus d’arguments i valors retornats d’una funció mitjançant anotacions de tipus. Aquestes anotacions no són obligatòries i no es comproven en temps d’execució, però ajuden a documentar el codi i milloren l’autocompletat i la detecció d’errors per part d’eines com mypy, Pyright o editors com VSCode.
def saluda(nom: str, edat: int) -> str:
return f"Hola, {nom}. Tens {edat} anys."
Els tipus s’especifiquen amb el format:
def nom_funció(paràmetre: Tipus) -> TipusRetorn:
El mòdul typing permet descriure tipus més complexos:
| Tipus | Descripció |
|---|---|
List[T] | Llista d’elements del tipus T |
Dict[K, V] | Diccionari amb claus de tipus K i valors V |
Optional[T] | Valor que pot ser de tipus T o None |
Tuple[T1, T2] | Tupla amb elements de tipus T1, T2, etc. |
Union[T1, T2] | Valor que pot ser de tipus T1 o T2 |
Any | Qualsevol tipus |
Exemple amb List i Dict:
from typing import List, Dict
def suma_llista(nums: List[int]) -> int:
return sum(nums)
def traduccions(cat2eng: Dict[str, str]) -> None:
for cat, eng in cat2eng.items():
print(f"{cat} → {eng}")
Exemple amb Optional:
from typing import Optional
def busca(llista: List[int], valor: int) -> Optional[int]:
if valor in llista:
return valor
return None
Aquest exemple indica que la funció pot retornar un enter o None.
Consells:
- Per a col·leccions a partir de Python 3.9, es poden usar tipus natius:
list[int],dict[str, int], etc. - En versions anteriors, cal usar typing.List, typing.Dict, etc.
- Les anotacions ajuden els humans i eines automàtiques, però no canvien el comportament del codi.
Python idiomàtic
Aquesta secció recull construccions, funcions i idiomes habituals de Python que apareixen constantment quan es treballa amb dades, machine learning (ML) i data science (DS).
-
enumerate(iterable)→ índex i valor alhoraQuan recorrem una col·lecció, sovint necessitem tant la posició com el valor.
enumerategenera parells (índex, element) automàticament.for i, valor in enumerate(["a", "b", "c"]): print(i, valor)# 0 a / 1 b / 2 c -
zip→ agrupar iterables en paral·lelPermet recórrer múltiples col·leccions simultàniament, agrupant elements per posició. S’atura quan s’esgota la col·lecció més curta.
noms = ["A", "B", "C"] notes = [7, 8, 9] for nom, nota in zip(noms, notes): print(nom, nota) -
all,any→ comprovar condicions sobre col·leccionsallretornaTruesi tots els elements compleixen una condició.anyretornaTruesi almenys un element la compleix. Útils per validacions ràpides.notes = [7, 8, 9] print(all(n > 5 for n in notes)) # True print(any(n == 10 for n in notes)) # False -
sorted(iterable, key=...)Retorna una nova llista ordenada sense modificar l’original. El paràmetre
keypermet especificar una funció per determinar el criteri d’ordenació.paraules = ["cotxe", "aigua", "lluna"] print(sorted(paraules, key=len)) # ['aigua', 'lluna', 'cotxe'] -
Desempaquetament múltiple
Permet assignar múltiples valors en una sola instrucció. L’operador
*captura elements restants en una llista.a, b = (1, 2) a, *rest, b = [1, 2, 3, 4, 5] # a=1, rest=[2,3,4], b=5 -
Slicing avançat (essencial per arrays i tensors)
La notació
[inici:final:pas]permet extreure subseqüències. És fonamental per manipular arrays de NumPy i tensors en ML, on sovint es treballa amb porcions de dades.llista = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] print(llista[2:7]) # [2, 3, 4, 5, 6] print(llista[::2]) # [0, 2, 4, 6, 8] (pas de 2) print(llista[::-1]) # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] (invertir) print(llista[1:8:2]) # [1, 3, 5, 7] -
map,filter,reduce→ programació funcionalApliquen funcions a col·leccions sense bucles explícits.
maptransforma cada element,filterselecciona elements que compleixen una condició, ireduceacumula valors.from functools import reduce nums = [1, 2, 3, 4] quadrats = list(map(lambda x: x**2, nums)) # [1, 4, 9, 16] parells = list(filter(lambda x: x % 2 == 0, nums)) # [2, 4] suma = reduce(lambda x, y: x + y, nums) # 10 -
*args,**kwargs→ arguments variablesPermeten que una funció accepti un nombre variable d’arguments.
*argscaptura arguments posicionals i**kwargscaptura arguments amb nom (keyword arguments).def mostra(*args, **kwargs): print(args, kwargs) mostra(1, 2, 3, nom="Anna", edat=25) # (1, 2, 3) {'nom': 'Anna', 'edat': 25} -
Gestió de paths amb
pathlibLa llibreria
pathlibofereix una manera orientada a objectes i multiplataforma de treballar amb rutes de fitxers. L’operador/construeix paths de forma elegant.from pathlib import Path fitxer = Path("dades") / "train.csv" if fitxer.exists(): contingut = fitxer.read_text() -
Lectura de fitxers amb
withEl context manager
withgaranteix que els fitxers es tanquin automàticament, fins i tot si hi ha errors. Evita problemes de memòria i recursos no alliberats.with open("dades.txt") as f: for linia in f: print(linia.strip()) -
Mòdul
csvProporciona funcions per llegir i escriure fitxers CSV de manera robusta, gestionant automàticament cometes, delimitadors i formats especials.
import csv with open("dades.csv") as f: lector = csv.reader(f) for fila in lector: print(fila) -
Walrus operator (Python 3.8+)
L’operador
:=permet assignar i utilitzar un valor en la mateixa expressió. Útil per evitar càlculs duplicats i fer codi més compacte.# Útil per processar dades línia a línia while (line := f.readline()): process(line)
Referències
NumPy
- Array Creation and Inspection
- NumPy Broadcasting and Universal Functions (ufuncs)
- NumPy Linear Algebra & Matrix Operations
- NumPy Reshaping, Indexing & Random Sampling
Array Creation and Inspection
This notebook covers creating NumPy arrays and inspecting their properties.
import numpy as np
np.random.seed(0)
Array Creation
np.arraynp.zeros,np.ones,np.full,np.eye- Random arrays:
np.random.rand,np.random.randn
a = np.array([1, 2, 3])
b = np.zeros((2,3))
c = np.ones((3,3))
d = np.full((2,2), 7)
e = np.eye(4)
rand = np.random.rand(2,3)
print(a, b, c, d, e, rand, sep='\n')
[1 2 3]
[[0. 0. 0.]
[0. 0. 0.]]
[[1. 1. 1.]
[1. 1. 1.]
[1. 1. 1.]]
[[7 7]
[7 7]]
[[1. 0. 0. 0.]
[0. 1. 0. 0.]
[0. 0. 1. 0.]
[0. 0. 0. 1.]]
[[0.5488135 0.71518937 0.60276338]
[0.54488318 0.4236548 0.64589411]]
Data Types and Conversion
print(a.dtype)
a_float = a.astype(float)
print(a_float.dtype)
int64
float64
Advanced: Structured Arrays
dt = np.dtype([('name','U10'), ('age','i4'), ('weight','f4')])
people = np.array([('Alice', 25, 55.0), ('Bob', 30, 85.5)], dtype=dt)
print(people['name'], people['age'], people['weight'])
['Alice' 'Bob'] [25 30] [55. 85.5]
NumPy Broadcasting and Universal Functions (ufuncs)
Learn how NumPy handles operations between arrays of different shapes and leverages its fast ufuncs.
import numpy as np
np.random.seed(1)
# Sample arrays
a = np.arange(6).reshape(2,3)
b = np.array([1, 2, 3])
print('a:\n', a)
print('b:', b)
a:
[[0 1 2]
[3 4 5]]
b: [1 2 3]
Broadcasting Basics
- When dimensions differ, NumPy ‘stretches’ the smaller array along the missing axes.
- Rules: trailing dimensions must match or be 1.
# Broadcasting example
c = a + b # b is broadcast over rows
print('a + b =\n', c)
a + b =
[[1 3 5]
[4 6 8]]
Broadcasting with Higher Dimensions
- Example: adding a (2,3,1) array to a (3,) array
x = np.arange(6).reshape(2,3,1)
y = np.array([10, 20, 30])
print('x shape:', x.shape)
print('y shape:', y.shape)
z = x + y # y broadcast across last two dims
print('z shape:', z.shape)
print(z)
x shape: (2, 3, 1)
y shape: (3,)
z shape: (2, 3, 3)
[[[10 20 30]
[11 21 31]
[12 22 32]]
[[13 23 33]
[14 24 34]
[15 25 35]]]
Universal Functions (ufuncs)
- Fast elementwise operations implemented in C.
- Examples:
np.sin,np.exp,np.add,np.multiply.
print('sin(a):\n', np.sin(a))
print('exp(a):\n', np.exp(a))
sin(a):
[[ 0. 0.84147098 0.90929743]
[ 0.14112001 -0.7568025 -0.95892427]]
exp(a):
[[ 1. 2.71828183 7.3890561 ]
[ 20.08553692 54.59815003 148.4131591 ]]
ufunc Methods: reduce, accumulate, outer
reduce: apply ufunc to collapse an axisaccumulate: cumulative applicationouter: all-pairs operation
print('add.reduce(a):', np.add.reduce(a, axis=1))
print('add.accumulate(a):\n', np.add.accumulate(a, axis=1))
print('multiply.outer([1,2], [3,4]):\n', np.multiply.outer([1,2], [3,4]))
add.reduce(a): [ 3 12]
add.accumulate(a):
[[ 0 1 3]
[ 3 7 12]]
multiply.outer([1,2], [3,4]):
[[3 4]
[6 8]]
Vectorized String Operations
np.charmodule provides vectorized string ops.
s = np.array(['apple', 'banana', 'cherry'])
print('Uppercase:', np.char.upper(s))
print('Replace a->@:', np.char.replace(s, 'a', '@'))
Uppercase: ['APPLE' 'BANANA' 'CHERRY']
Replace a->@: ['@pple' 'b@n@n@' 'cherry']
NumPy Linear Algebra & Matrix Operations
Perform matrix products, decompositions, and other linear algebra routines.
import numpy as np
from numpy import linalg as LA
np.random.seed(0)
# Define sample matrices
A = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
B = np.array([[9, 8, 7], [6, 5, 4], [3, 2, 1]])
print('A:')
print(A)
print('\nB:')
print(B)
A:
[[1 2 3]
[4 5 6]
[7 8 9]]
B:
[[9 8 7]
[6 5 4]
[3 2 1]]
Matrix Multiplication
@operator ornp.dot
C1 = A @ B
C2 = np.dot(A, B)
print('A @ B =')
print(C1)
print('\nnp.dot(A, B) =')
print(C2)
A @ B =
[[ 30 24 18]
[ 84 69 54]
[138 114 90]]
np.dot(A, B) =
[[ 30 24 18]
[ 84 69 54]
[138 114 90]]
Elementwise Operations
*,np.multiply,np.add
print('A * B =')
print(A * B)
print('\nnp.multiply(A, B) =')
print(np.multiply(A, B))
A * B =
[[ 9 16 21]
[24 25 24]
[21 16 9]]
np.multiply(A, B) =
[[ 9 16 21]
[24 25 24]
[21 16 9]]
Matrix Inverse, Determinant, and Rank
LA.inv,LA.det,LA.matrix_rank
# Use a smaller invertible matrix
M = np.array([[4, 7], [2, 6]])
print('M:', M)
print('Inverse of M:', LA.inv(M))
print('Determinant of M:', LA.det(M))
print('Rank of A:', LA.matrix_rank(A))
M: [[4 7]
[2 6]]
Inverse of M: [[ 0.6 -0.7]
[-0.2 0.4]]
Determinant of M: 10.000000000000002
Rank of A: 2
Eigenvalues & Eigenvectors
LA.eig
eigvals, eigvecs = LA.eig(M)
print('Eigenvalues:', eigvals)
print('Eigenvectors:')
print(eigvecs)
Eigenvalues: [1.12701665 8.87298335]
Eigenvectors:
[[-0.92511345 -0.82071729]
[ 0.37969079 -0.57133452]]
Solving Linear Systems
- Solve Ax = b via
LA.solve
b = np.array([1, 2])
x = LA.solve(M, b)
print('Solution x for Mx = b:', x)
# Verify Mx
print('M @ x:', M @ x)
Solution x for Mx = b: [-0.8 0.6]
M @ x: [1. 2.]
Pseudo-Inverse and Least Squares
LA.pinv,LA.lstsq
# Overdetermined system example
X = np.random.randn(5, 3)
y = np.random.randn(5)
# Least squares solution
coef, residuals, rank, s = LA.lstsq(X, y, rcond=None)
print('Coefficients:', coef)
print('Residuals:', residuals)
# Pseudo-inverse solution
coef_pinv = LA.pinv(X) @ y
print('Coefficients via pinv:', coef_pinv)
Coefficients: [-0.22776487 1.10638653 0.07697564]
Residuals: [0.82230318]
Coefficients via pinv: [-0.22776487 1.10638653 0.07697564]
Tensor Dot and Trace
np.trace,np.tensordot
print('Trace of A:', np.trace(A))
print('Tensor dot A and B over axes (1,0):')
print(np.tensordot(A, B, axes=(1, 0)))
Trace of A: 15
Tensor dot A and B over axes (1,0):
[[ 30 24 18]
[ 84 69 54]
[138 114 90]]
NumPy Reshaping, Indexing & Random Sampling
Master reshaping arrays, indexing techniques, and generating random samples for experiments.
import numpy as np
np.random.seed(2)
arr = np.arange(24)
print('Original arr shape:', arr.shape)
Original arr shape: (24,)
Reshaping Arrays
reshape(new_shape)ravel(),flatten()transpose(),swapaxes()
print('reshape to (4,6):')
print(arr.reshape(4,6))
print('\nreshape to (2,3,4):')
print(arr.reshape(2,3,4))
reshape to (4,6):
[[ 0 1 2 3 4 5]
[ 6 7 8 9 10 11]
[12 13 14 15 16 17]
[18 19 20 21 22 23]]
reshape to (2,3,4):
[[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]
[[12 13 14 15]
[16 17 18 19]
[20 21 22 23]]]
Flatten vs Ravel
flatten(): returns a copyravel(): returns a view when possible
flat = arr.flatten()
rv = arr.ravel()
flat[0] = 100
rv[1] = 200
print('After modifying flat vs ravel:')
print('Original arr[:2]:', arr[:2])
After modifying flat vs ravel:
Original arr[:2]: [ 0 200]
Advanced Indexing Techniques
- Integer array indexing
- Boolean masks
take()andput()
# Integer indexing
idx = np.array([0, 2, 5])
print('Selected elements:', arr[idx])
# Boolean mask
mask = arr % 2 == 0
print('Even elements:', arr[mask])
Selected elements: [0 2 5]
Even elements: [ 0 200 2 4 6 8 10 12 14 16 18 20 22]
Adding and Removing Dimensions
newaxisexpand_dims(),squeeze()
a = np.arange(6)
print('a shape:', a.shape)
a2 = a[np.newaxis, :]
print('a with new axis:', a2.shape)
a3 = np.expand_dims(a, axis=1)
print('expand_dims axis=1:', a3.shape)
print('squeeze back:', np.squeeze(a3).shape)
a shape: (6,)
a with new axis: (1, 6)
expand_dims axis=1: (6, 1)
squeeze back: (6,)
Random Sampling
rand,randn,randint,random_samplechoice,shuffle
print('rand 2x3:', np.random.rand(2,3))
print('randn 3x3:', np.random.randn(3,3))
print('randint 0-10:', np.random.randint(0, 10, size=5))
print('random_sample 5:', np.random.random_sample(5))
rand 2x3: [[0.4359949 0.02592623 0.54966248]
[0.43532239 0.4203678 0.33033482]]
randn 3x3: [[ 0.50288142 -1.24528809 -1.05795222]
[-0.90900761 0.55145404 2.29220801]
[ 0.04153939 -1.11792545 0.53905832]]
randint 0-10: [1 3 5 8 4]
random_sample 5: [0.29701836 0.28786882 0.11619332 0.18172704 0.49428977]
Permutations and Choice
shufflefor in-place permutingchoicefor sampling with/without replacement
arr2 = np.arange(10)
np.random.shuffle(arr2)
print('Shuffled:', arr2)
print('Choice 5 elements:', np.random.choice(arr2, size=5, replace=False))
Shuffled: [5 3 8 6 7 0 1 9 4 2]
Choice 5 elements: [0 7 6 1 8]
Practical: Create Train/Test Split
Using random sampling to split data arrays.
X = np.arange(20).reshape(10,2)
y = np.arange(10)
# Shuffle indices
indices = np.arange(10)
np.random.shuffle(indices)
# 80/20 split
split = int(0.8 * len(indices))
train_idx = indices[:split]
test_idx = indices[split:]
X_train, X_test = X[train_idx], X[test_idx]
y_train, y_test = y[train_idx], y[test_idx]
print('Train X:', X_train)
print('Test X:', X_test)
Train X: [[ 2 3]
[12 13]
[10 11]
[ 8 9]
[ 4 5]
[14 15]
[16 17]
[ 0 1]]
Test X: [[ 6 7]
[18 19]]
Matplotlib
Matplotlib permet crear tot tipus de gràfics en Python: línies, barres, pastís, dispersió, histogrames i més. Primer generes les dades (normalment amb NumPy), després “construeixes” el gràfic afegint elements com etiquetes, títols, llegendes i quadrícules. Finalment, plt.show() el mostra. Cada comanda que dibuixa dades afegeix un nou element visual, i pots personalitzar-los amb colors, tipus de línia, formes i altres estils. També és possible crear diverses figures i subplots per mostrar diferents gràfics dins del mateix espai, cosa que facilita comparar dades i visualitzacions.
Basics: Common Plot Types
Visualize data using line plots, scatter plots, histograms, bar charts, and pie charts.
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(3)
Line Plot
- Use
plt.plot()for continuous data. - Add labels, title, grid, and legend.
x = np.linspace(0, 2 * np.pi, 200)
y1 = np.sin(x)
y2 = np.cos(x)
plt.plot(x, y1, label='sin(x)')
plt.plot(x, y2, label='cos(x)', linestyle='--')
plt.xlabel('x')
plt.ylabel('f(x)')
plt.title('Sine and Cosine Waves')
plt.legend()
plt.grid(True)
plt.show()

Scatter Plot
plt.scatter()for showing individual data points.- Color and size can represent additional dimensions.
x = np.random.rand(100)
y = np.random.rand(100)
colors = np.random.rand(100)
sizes = 100 * np.random.rand(100)
plt.scatter(x, y, c=colors, s=sizes, alpha=0.6, cmap='viridis')
plt.colorbar(label='Color scale')
plt.title('Bubble Scatter Plot')
plt.xlabel('x')
plt.ylabel('y')
plt.show()

Histogram
plt.hist()to visualize distributions.- Adjust bins and density.
data = np.random.randn(1000)
plt.hist(data, bins=30, density=True, alpha=0.7)
plt.title('Normalized Histogram of Gaussian Data')
plt.xlabel('Value')
plt.ylabel('Density')
plt.show()

Bar Chart
plt.bar()for categorical comparisons.- Horizontal bar:
plt.barh()
categories = ['A', 'B', 'C', 'D']
values = [5, 7, 3, 4]
plt.bar(categories, values, alpha=0.8)
plt.title('Vertical Bar Chart')
plt.xlabel('Category')
plt.ylabel('Value')
plt.show()
# Horizontal bar chart
plt.barh(categories, values, alpha=0.8)
plt.title('Horizontal Bar Chart')
plt.xlabel('Value')
plt.ylabel('Category')
plt.show()


Pie Chart
plt.pie()for composition of a whole.
labels = ['W', 'X', 'Y', 'Z']
sizes = [15, 30, 45, 10]
explode = (0, 0.1, 0, 0) # only 'explode' the 2nd slice
plt.pie(sizes, labels=labels, autopct='%1.1f%%', explode=explode, shadow=True)
plt.title('Pie Chart Example')
plt.axis('equal') # Equal aspect ensures pie is drawn as a circle.
plt.show()

Saving Figures
- Use
plt.savefig()to export plots.
# Example: Save the sine wave plot
x = np.linspace(0, 2 * np.pi, 200) # Redefine x for the sine wave
y1 = np.sin(x) # Redefine y1 based on the correct x
plt.figure()
plt.plot(x, y1)
plt.title('Sine Wave')
plt.savefig('data/sine_wave.png', dpi=150)
print('Saved figure as sine_wave.png')
Saved figure as sine_wave.png

Subplots & Advanced Plots
Learn to create complex layouts and advanced visualizations such as error bars and images.
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(4)
Creating Subplots
plt.subplots(nrows, ncols)to create grid of axes.- Adjust layouts with
figsize,tight_layout.
fig, axs = plt.subplots(2, 2, figsize=(10, 8))
axs[0, 0].plot(np.sin(np.linspace(0, 2*np.pi, 100)))
axs[0, 0].set_title('Sine Wave')
axs[0, 1].bar(['A', 'B', 'C'], [3, 5, 2])
axs[0, 1].set_title('Bar Plot')
axs[1, 0].hist(np.random.randn(500), bins=20)
axs[1, 0].set_title('Histogram')
axs[1, 1].scatter(np.random.rand(50), np.random.rand(50), c=np.random.rand(50), cmap='plasma')
axs[1, 1].set_title('Scatter Plot')
plt.tight_layout()
plt.show()

Shared Axes and Figure-Level Settings
- Share x or y axes across subplots.
- Add a main title with
fig.suptitle().
fig, axs = plt.subplots(2, 1, sharex=True, figsize=(8, 6))
x = np.linspace(0, 10, 100)
axs[0].plot(x, np.sin(x))
axs[0].set_title('Sine')
axs[1].plot(x, np.cos(x))
axs[1].set_title('Cosine')
fig.suptitle('Shared X-Axis Example')
plt.show()

Error Bars
- Use
plt.errorbar()to show uncertainties.
x = np.arange(5)
y = np.random.rand(5)
yerr = 0.1 + 0.2 * np.random.rand(5)
plt.errorbar(x, y, yerr=yerr, fmt='o-', capsize=5)
plt.title('Error Bars Example')
plt.xlabel('X')
plt.ylabel('Y')
plt.show()

Image Plot
- Display 2D arrays as images with
plt.imshow().
img = np.random.rand(10, 10)
plt.imshow(img, interpolation='nearest')
plt.title('Random Image')
plt.colorbar()
plt.show()

Advanced: Contour and Heatmap
- Use
plt.contour()andplt.imshow()for heatmaps.
x = np.linspace(-3, 3, 100)
y = np.linspace(-3, 3, 100)
X, Y = np.meshgrid(x, y)
Z = np.exp(-(X**2 + Y**2))
plt.figure(figsize=(6,5))
contours = plt.contour(X, Y, Z, levels=6)
plt.clabel(contours, inline=True)
plt.title('Contour Plot')
plt.show()
# Heatmap
plt.figure(figsize=(6,5))
plt.imshow(Z, origin='lower', extent=[-3,3,-3,3])
plt.title('Heatmap')
plt.colorbar()
plt.show()


Example project
This section guides you through generating, analyzing, and visualizing a synthetic dataset for classification.
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification
np.random.seed(5)
Generate Synthetic Data
- Use
make_classificationto create a dataset.
X, y = make_classification(
n_samples=200,
n_features=4,
n_informative=2,
n_redundant=0,
n_clusters_per_class=1,
flip_y=0.01,
class_sep=1.5,
random_state=5
)
print('Features shape:', X.shape)
print('Labels distribution:', np.bincount(y))
Features shape: (200, 4)
Labels distribution: [ 99 101]
Explore Feature Distributions
- Plot histograms for each feature.
fig, axs = plt.subplots(2, 2, figsize=(10, 8))
for i in range(4):
ax = axs.flat[i]
ax.hist(X[:, i], bins=20, color='C{}'.format(i), alpha=0.7)
ax.set_title(f'Feature {i} Distribution')
plt.tight_layout()
plt.show()

Pairwise Scatter Plots
- Visualize relationships between pairs of features.
fig, axs = plt.subplots(2, 3, figsize=(12, 8))
pairs = [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]
for ax, (i, j) in zip(axs.flat, pairs):
ax.scatter(X[:, i], X[:, j], c=y, cmap='coolwarm', alpha=0.6)
ax.set_xlabel(f'Feature {i}')
ax.set_ylabel(f'Feature {j}')
ax.set_title(f'{i} vs {j}')
plt.tight_layout()
plt.show()

Compute Basic Statistics
- Calculate means and standard deviations per class.
classes = np.unique(y)
for cls in classes:
cls_data = X[y == cls]
print(f'Class {cls}: mean={cls_data.mean(axis=0)}, std={cls_data.std(axis=0)}')
Class 0: mean=[-1.51904518 -0.13961004 -1.51551924 -0.10388006], std=[0.94565383 0.92078405 0.53456453 0.88192832]
Class 1: mean=[-1.62540423 0.03701214 1.51590761 0.1083452 ], std=[0.85187348 0.96153734 0.53164293 0.91483422]
Train/Test Split & Visualization
- Split data using NumPy and plot class distributions.
indices = np.arange(len(y))
np.random.shuffle(indices)
split = int(0.8 * len(indices))
train_idx, test_idx = indices[:split], indices[split:]
X_train, X_test = X[train_idx], X[test_idx]
y_train, y_test = y[train_idx], y[test_idx]
# Visualize train vs test in feature 0 & 1
plt.figure(figsize=(6,5))
plt.scatter(X_train[:,0], X_train[:,1], c=y_train, cmap='viridis', label='Train', alpha=0.6)
plt.scatter(X_test[:,0], X_test[:,1], c=y_test, cmap='coolwarm', marker='x', label='Test', alpha=0.6)
plt.xlabel('Feature 0')
plt.ylabel('Feature 1')
plt.legend()
plt.title('Train vs Test Data')
plt.show()

Save Processed Data
- Optionally save arrays to disk for later use.
np.save('data/X_train.npy', X_train)
np.save('data/X_test.npy', X_test)
np.save('data/y_train.npy', y_train)
np.save('data/y_test.npy', y_test)
print('Saved .npy files in data directory.')
Saved .npy files in data directory.
Fonaments de programació
Principis
Programari
Un cop carregat un programari, la memòria principal d’un ordinador conté instruccions i dades. La CPU executa instruccions màquina, que són instruccions de molt baix nivell que processen dades.
El programari s’expressa habitualment utilitzant un llenguatge de programació, una abstracció que amaga el funcionament de la CPU basat en instruccions màquina.
Un programari es pot descriure pel seu comportament i el seu estat:
- El comportament es defineix amb un flux d’instruccions depenent de les dades.
- Les instruccions poden:
- Fer assignacions, o sigui, modificar l’estat
- Bifurcar-se segons condicions (expressions lògiques) a partir de l’estat
- Llegir dades externes i incorporar-les a l’estat
- Escriure dades utilitzant l’estat

El paradigma més habitual de la programació és l’imperatiu. Aquest, utilitza sentències per a modificar l’estat d’un programa.
Codi
La programació estructurada és una característica dels llenguatges de programació que millora la claredat i qualitat del codi. En tenim:
- Control de flux (if/then/else)
- Repetició (while/for)
- Blocs o àmbits de codi
- Subrutines: procediments i funcions
Dades
Les dades poden ser de diferents tipus:
- Primitius: el tipus més bàsic. Numèrics, caràcters, booleans.
- Estructures: més d’una dada. Per exemple, un array o una llista.
- Referències: apuntadors a altres dades (normalment estructures) utilitzant la seva adreça de memòria.
Operacions per a una dada:
- Reservar espai en memòria (també anomenat instanciar).
- Assignar el seu valor (instrucció d’assignació).
- Obtenir el seu valor.
- Eliminar la dada, alliberant l’espai. Pot ser automàtic (Garbage Collection).
Una variable és un nom simbòlic que s’associa a una dada continguda en memòria.
L’assignació d’una variable sol tenir el format variable = expressió. Això implica avaluar l’expressió i assignar el valor a la variable, reemplaçant el valor anterior.
La dada associada a una variable pot ser mutable o immutable. És mutable si pot ser modificada, i immutable si no. Habitualment, els primitius són immutables.
Funcions
Les funcions són agrupacions de codi reutilitzables que podem cridar utilitzant paràmetres. Aquests paràmetres tenen un àmbit reduït al cos de la funció. Poden tenir un valor de retorn.
Les funcions poden:
- Tenir efectes secundaris: si es modifica l’estat fora del seu àmbit. Per exemple, modificant una variable d’un àmbit més global, o un paràmetre mutable. Això és possible si utilitzem referències.
- Ser pures, si la funció sempre retorna el mateix valor per als mateixos paràmetres i no té efectes secundaris.
Les crides a funcions es gestionen utilitzant una espai de memòria anomenat stack. Cada cop que es fa una crida, creem un nou frame i l’afegim a dalt del stack. A dalt del stack sempre hi ha el frame actiu. Durant la seva execució, aquest frame guarda les dades que s’utilitzen. Quan es fa return, s’elimina aquest frame, i el de sota passa a ser l’actiu.
Objectes
Alguns llenguatges permeten classes, que són plantilles per a crear instàncies d’objectes. Un objecte està compost de:
- Codi que s’executa mitjançant mètodes o operacions (funcions de l’objecte).
- Estat, o dit d’una altra forma, les seves dades associades, habitualment privades.
Un objecte que té operacions que poden modificar el seu estat es diu que és mutable. Si en canvi no es pot, l’objecte és immutable.
Quan un objecte no té comportament, només dades, se l’anomena estructura de dades. Les seves operacions exposen aquestes dades.
Memòria
Tenim dos espais de la memòria principal on es guarden dades: el stack (pila) i el heap (munt).
Un frame del stack s’utilitza dins de l’àmbit d’una funció o mètode per a guardar paràmetres, primitius i referències.
El heap s’utilitza per a guardar estructures, ja que les estructures poden sobreviure l’àmbit on es creen mitjançant les seves referències. En canvi, com que els primitius no es poden referenciar, es creen al stack i que moren amb el return.
Expressions
Una expressió aritmètica té un valor numèric. Es forma utilitzant operadors aritmètics sobre variables, constants o funcions numèriques.
Una expressió lògica té dos possibles valors: True o False. Es formen utilitzant variables i constants lògiques utilitzant operadors lògics (NOT, AND, OR…) i relacionals (=, <, >, <=, >=, <>).
Parèntesis i prioritat
Els operadors de les expressions s’apliquen en funció de la seva prioritat.
Per als aritmètics (de més a menys prioritat):
- ++, –
- *, /, %
- +, -
Per als lògics (de més a menys prioritat):
- NOT
- AND
- OR
- operadors relacionals
Es pot prioritzar una operació lògica o aritmètica utilitzant parèntesis ().
Lleis booleanes
- Commutativa:
- a OR b = b OR a
- a AND b = b AND a
- Identitat:
- a OR False = a
- h AND True = h
- Complement:
- a OR NOT(a) = True
- a AND NOT(a) = False
- Idempotent:
- a OR a = a
- a AND a = a
- Doble negació:
- NOT(NOT(a)) = a
- Associativitat:
- a OR b OR c = (a OR b) OR c = a OR (b OR c)
- a AND b AND c = (a AND b) AND c = a AND (b AND c)
- Distributiva:
- a AND (b OR c) = (a AND b) or (a AND c)
- a OR (b AND c) = (a OR b) AND (a OR c)
- De Morgans:
- NOT(a AND b) = NOT(a) OR NOT(b)
- NOT (a OR b) = NOT(a) AND NOT(b)
- Absorció:
- a AND (a OR b) = a
- a OR (a AND b) = a
Paradigmes
Un paradigma de programació és una forma d’estructurar i representar un programa. Cada llenguatge de programació pot suportar un o més paradigmes.
Aquests són alguns dels paradigmes més presents als llenguatges:
- Imperatiu: el codi té un flux d’execució amb sentències que poden canviar l’estat (és mutable). Aquest concepte s’ha perfeccionat amb altres paradigmes com l’estructurat o el procedural, afegint procediments, variables locals, seqüències, iteració, etc.
- Orientat a objecte: estil procedural que amaga (encapsula) l’estat darrere del concepte d’objecte. Els objectes exposen mètodes que manipulen aquest estat.
- Basat en esdeveniments: estil procedural on el flux d’execució és controlat per l’emissió d’esdeveniments, que poden arribar a qualsevol consumidor. Sol haver una cua al sistema que els despatxa.
- Basat en missatges: similar als esdeveniments, però el que s’envia són missatges, i es fa cap a una adreça concreta. Sol haver una cua al receptor per poder gestionar-los quan estigui preparat.
- Asíncron: en contraposició a la programació síncrona, s’executen tasques, habitualment de forma simultània, sense esperar que es completin per a prosseguir. Associat a conceptes com callbacks, promeses, futurs o async/await.
- Reactiu: programació declarativa basada en esdeveniments asíncrons i fluxos de dades. Es basen en APIs d’operacions on els observadors tenen callbacks per gestionar l’èxit i l’error. Els fluxos poden ser pull i push (back pressure).
- Funcional: les operacions (o funcions) són matemàtiques i no hi ha estat. Les funcions permeten altres funcions com a paràmetres, i totes les dades són immutables. L’objectiu és no tenir efectes secundaris al codi.
- Orientat a dades: estil alhora procedural i funcional. El codi modela únicament les dades, que són immutables. Les operacions estan completament separades de les dades.
- Declaratiu: el codi defineix una lògica, però no diu res respecte del flux d’execució.
- Multi-fil: se’n diu de la possibilitat de dissenyar programes amb més d’un fil d’execució treballant coordinadament.
A continuació es mostren alguns llenguatges de programació els paradigmes principals que els defineixen (tot i permetre altres):
- Java: procedural, orientat a objecte, multi-fil
- Python: procedural, orientat a objecte
- JavaScript: procedural, asíncrona, basat en esdeveniments
- SQL: declaratiu
Conceptes addicionals
Concurrència
La concurrència és l’habilitat d’un sistema de poder executar diverses tasques alhora. En una aplicació es pot implementar de diverses maneres. Per exemple, amb multi-fil.
Mutabilitat
Es diu que una dada és mutable si pot ser modificada després de ser creada. Les dades immutables són més eficients i més útils. En programació concurrent es diu que són inherentment thread-safe, o sigui, no impliquen conflictes quan s’accedeixen per més d’un fil alhora.
Recursivitat
La recursivitat és una forma de resoldre problemes on la solució depèn de la solució a un problema més petit. La forma habitual en la qual la resolen els llenguatges de programació és permetent que una funció es pugui cridar a sí mateixa.
Efectes secundaris
Un efecte secundari es produeix quan una operació fa alguna cosa més que llegir els seus paràmetres i retornar un valor a qui l’ha cridada. Per exemple: llegir una variable no local, canviar un paràmetre mutable, llençar un error o fer E/S. D’una funció sense efectes secundaris se’n diu pura.
Referències
Disseny
- Tècniques per a la solució de problemes
- Com anomenar
- Bones pràctiques per escriure codi
- És dolent el teu codi?
- Principis generals de programació
- Principis de POO (SOLID)
- Dependències a POO
- Arquitectura i fronteres
- REST APIs
- Referències
Tècniques per a la solució de problemes
-
Definir el problema: el punt de partida i l’objectiu
- si el problema te l’ha donat un altre, explica’l amb les teves paraules
- representa el problema amb dibuixos i diagrames
- identifica les coses que no saps
-
Idear un pla
- Descomposició: trencar un problema en parts més senzilles (estructura d’arbre)
- Generalització: abstracció, identificar patrons i reduir el nombre de conceptes
- Patrons senzills: noms: objectes; verbs: operacions; adjectius: propietats; números: variables
- Patrons de control: bucles, subrutines, regles
- Altres tècniques:
- Pensament crític: posa en dubte les teves decisions… i si falla?
- Resoldre un problema concret
- Troba un problema relacionat
- Cercar cap enrere des de l’objectiu… com puc arribar?
- Dissenyar un model (simplificació, representació, dades, interacció)
-
Executar el pla
-
Revisar i estendre (iteració)
Com anomenar
Cada llenguatge de programació té les seves regles tipogràfiques i gramaticals per anomenar els seus elements. Per exemple, les regles tipogràfiques de Java són:
- Els paquets s’escriuen amb lletres minúscules, amb components separats per punts de més genèric a més específic:
org.junit.jupiter.api. - Les classes tenen una o més paraules, sense abreviatures, amb cadascuna en majúscules:
List,FutureTask. - Els mètodes i els camps segueixen la mateixa regla, però la primera lletra ha de ser minúscula:
remove,ensureCapacity. - Les variables locals segueixen el mateix criteri, però permeten abreviatures:
i,houseNum. - Els paràmetres de tipus són una lletra, on T és un tipus qualsevol, E un tipus d’element d’una col·lecció, K/V una clau i un valor, X una excepció, i R un tipus de retorn.
Els criteris gramaticals solen ser més flexibles:
- Les classes són noms o frases nominals:
Thread,PriorityQueue. - Les interfícies poden anomenar-se com les classes o bé com un adjectiu que acaba amb
ableoible:Runnable,Iterable. - Els mètodes que realitzen alguna acció són verbs o frases verbals:
append,drawImage. - Els mètodes que retornen un boolean solen començar per
isohas, i després poden seguir amb un nom, frase nominal o qualsevol paraula o frase que funcioni com adjectiu:isDigit,isProbablePrime,isEmpty,isEnabled,hasSiblings. - Els mètodes que retornen un valor no boolean o un atribut de l’objecte s’anomenen amb un nom o frase nominal, o una frase que comença per get:
size,hashCode,getTime. - Els mètodes getters i setters són una construcció obsoleta en molts casos, utilitzar-los amb cautela.
- Els camps de tipus boolean solen anomenar-se com l’accessor, però ometent ‘is’ o ‘has’:
initialized,started. - Els camps d’altres tipus no boolean solen ser noms o frases nominals:
height,digits,bodyStyle.
Bones pràctiques per escriure codi
Estàs escrivint codi per llegir-lo en el futur, o bé per un altre…
- Les classes han de ser petites, per sota de 500 línies, i han de tenir un nombre limitat de mètodes
- Els mètodes han de ser petits, per sota de 30 línies, i han de fer una feina concreta
- Has d’escriure el codi perquè s’expliqui a ell mateix, però on no arribis, utilitza comentaris
- No facis línies massa llargues, com a molt de 120 caràcters
- Manté baix el nivell de sagnat del codi, i intenta no superar els 3-4 nivells
- Si hi ha dades d’entrada, crea-les des del codi per no haver d’introduir-les des del teclat
- Anomena les classes, els mètodes i les variables amb els criteris ja explicats
- Decideix el teu estil i segueix-lo de forma consistent
És dolent el teu codi?
- És massa rígid? Es poden canviar els detalls interns d’aquest mòdul en el futur sense tocar el codi d’altres mòduls i altres capes? El codi rígid és el que té dependències que serpentegen en tantes direccions que no es pot fer un canvi aïllat sense canviar-ho tot al voltant.
- És massa fràgil? Seria difícil trobar llocs on fer canvis i refactoritzar en el futur? El codi fràgil es trenca de formes estranyes i que no es poden predir.
- Hauria de ser una característica reutilitzable? Si ho fos, el codi depèn de mòduls no desitjats que es podrien evitar? Vols una banana, però el que obtens és un goril·la agafant-la i tota la jungla amb ell.
Si mirem de prop, el fil conductor dels tres problemes esmentats és l’acoblament. Els mòduls depenen els uns dels altres de maneres no desitjades i resulten en un codi espagueti.
El codi hauria d’estar desacoblat entre els mòduls i les capes. Les polítiques d’alt nivell i les abstraccions no haurien de dependre de detalls de baix nivell, sinó d’abstraccions: caldria invertir la dependència dels mòduls als llocs necessaris. I escriure classes que només fan una cosa i només tenen un motiu per canviar.
El codi bo hauria d’explicar què està fent. Hauria de ser avorrit de llegir. Tot hauria de ser perfectament obvi. Això és bon codi - Robert Martin.
Principis generals de programació
- DRY (Don’t repeat yourself): no et repeteixis.
- Principi de l’abstracció: cada peça significant s’ha d’implementar en només un lloc del codi font.
- KISS (Keep it simple): mantenir la senzillesa.
- Evita crear YAGNI (no ho necessitaràs).
- Fes la feina més senzilla que sigui funcional.
- No em facis pensar.
- Escriu el codi per qui l’haurà de mantenir.
- El principi de la mínima sorpresa.
- Minimitzar l’acoblament i maximitzar la cohesió.
- Amagar els detalls de la implementació.
- La llei de Demeter: el codi només s’ha de comunicar amb les seves relacions directes.
- Evitar la optimització prematura: només si funciona i es lent.
- Reutilitzar codi és bo: el fa més llegible.
- Separació d’interessos: àrees de diferents funcionalitats han de tenir pocs solapaments.
- Els usuaris d’una classe han de dependre de la seva interfície pública, però la classe no ha de dependre dels usuaris.
Principis de POO (SOLID)
- Responsabilitat única (SRP): un component de codi ha de fer una sola feina i ben definida. Només caldrà modificar-lo si necessitem canviar aquesta feina. Això millora la cohesió i redueix possibles errors. Code smell: quan vull canviar una funcionalitat, una altra no relacionada queda afectada, i cal refer-la.
- Obert/tancat: les entitats software han d’estar obertes a ser esteses i tancades a ser modificades. Si cal estendre la funcionalitat és millor afegir codi que canviar l’existent. Això es pot fer amb abstracció, derivant classes i utilitzant polimorfisme, i amb encapsulació. Code smell: quan vull afegir funcionalitat, es produeix una cascada de canvis.
- Substitució Liskov: qualsevol classe que hereta d’una altra (pare) pot ser utilitzada d’igual forma que la pare sense conèixer les diferències entre elles. Per tant, quan heretem no hem de canviar el comportament que defineix la classe pare.
- Segregació d’interfície: es millor tenir moltes interfícies de client específiques que una sola de propòsit general. Així, evitem que els clients depenguin de mètodes que no utilitzen.
- Inversió de dependència: les dependències han de ser sobre abstraccions, no sobre concrecions. Es resumeix en dues qüestions:
- Els mòduls d’alt nivell (més abstractes) no han d’importar res de mòduls de baix nivell (més concrets). Els dos han de dependre d’abstraccions (p. ex. interfícies).
- Les abstraccions no han de dependre dels detalls. Són els detalls (implementacions concretes) les que han de dependre de les abstraccions.

Dependències a POO
A POO, si tenim dues classes A i B, i A necessita a B per fer la seva feina, llavors A té una dependència de B. Per a resoldre aquesta dependència podem fer:
- Que A crei o obtingui un un objecte B. La classe A té el control de la dependència.
- Que A rebi un objecte B. Algú altre li proporciona, sense que A s’hagi de preocupar.
A més, quan necessitem un objecte poden passar almenys dues coses:
- Que necessitem una nova instància cada cop. Per exemple, les factories creen múltiples objectes.
- Que necessitem una única instància compartida. Aquesta situació es relaciona amb el patró singleton.
La inversió de control (IoC) és un principi de POO que cedeix a un contenidor o framework la tasca de controlar la creació d’instàncies d’objectes.
Tenim principalment dues formes d’implementar IoC:
- L’injecció de dependència (DI), on un contenidor pren el control i fa crides al nostre codi per proporcionar les dependències d’un objecte. Hi ha principalment dos tipus: de construcció i de setter.
- El Service Locator, que introdueix un nou objecte al nostre codebase, el Locator, que permet resoldre dependències d’una certa classe.
Aquestes tècniques es poden implementar per a subministrar instàncies úniques o múltiples. Per a permetre instàncies úniques, com serveis, utilitzen un mapa o diccionari d’instàncies accessibles pel nom de la classe.
L’objectiu d’aquestes tècniques és seguir el principi d’inversió de dependència: fem les dependències sobre abstraccions (interfícies). Això permet separar l’ús de la construcció, i podem substituir les implementacions sense afectar el codi.
Arquitectura i fronteres
Definició de frontera
L’arquitectura d’un sistema defineix com es divideix en components, quines són les fronteres o límits (boundaries) d’aquests components i com es comuniquen a través d’aquestes. Quan no hi ha fronteres parlem del monòlit.
El propòsit d’aquestes fronteres és facilitar el seu desenvolupament, gestió, manteniment i evolució. Una bona definició de les fronteres redueix l’acoblament, facilitant la flexibilitat de modificar unes parts sense afectar unes altres, i evita la degradació gradual de l’arquitectura. D’això se’n diu arquitectura evolutiva.
- Facilita el desenvolupament, perquè permet desenvolupar les parts de forma independent, a ritmes diferents.
- Facilita les proves, podem crear objectes que simulen comportament (test doubles), com dummies, stubs, mocks o fakes.
- Facilita els canvis i l’evolució, ja que podem canviar la implementació d’un comportament sense afectar la resta.
Les dependències entre components haurien de ser un graf acíclic dirigit. Això evita les dependències circulars, que augmenta l’acoblament dels components i limita la possibilitat de reutilitzar-los de forma individual.
Tipologia de fronteres
Tipus de separació
Existeixen dos enfocaments principals per crear fronteres en sistemes de programari: la separació horitzontal i la separació vertical.
- L’horitzontal crea les fronteres entre àrees tècniques del sistema. Per exemple, una API, la lògica de negoci i la comunicació amb la base de dades. Els canvis impliquen habitualment diferents capes del sistema. Pot ser un problema si les capes les gestionen diferents equips de desenvolupament.
- La vertical crea la frontera entre àrees funcionals del sistema. S’utilitza amb microserveis. Per exemple, la gestió d’usuaris o la creació de comandes. Els canvis en aquest tipus de separació són més àgils.
Mecanismes de separació
Les fronteres es poden implementar mitjançant quatre mecanismes diferents:
- Codi font: utilitzant classes i interfícies per poder comunicar-se mitjançant mètodes sense veure la implementació. Si es fa bé (actuant amb bona fe), permet aïllar les parts per permetre múltiples equips treballant. És l’únic mecanisme dels monòlits.
- Components vinculats dinàmicament: es fa una separació amb components desplegables, per exemple, arxius JAR. Es comuniquen amb crides a mètodes, i poden utilitzar el principi d’inversió de dependència per a establir les relacions entre ells.
- Processos locals: tenim processos locals que estan a la mateixa màquina. Poden comunicar-se utilitzant memòria compartida o sòcols. Permeten utilitzar diferents entorns de desenvolupament i tecnologies, sempre que es comparteixi el protocol.
- Serveis: permet que els serveis estiguin a diferents màquines i utilitzin la xarxa. No se sol compartir la base de dades (mala pràctica). S’estableixen protocols estàndards basats en la xarxa, com REST. Exemple dels microserveis.
Resumint, els mecanismes de separació es poden classificar segons el seu enfocament:
- Basats en interfícies: Codi font i components vinculats dinàmicament.
- Basats en protocols: Processos locals i serveis.
Disseny per contracte
La correcta definició de la frontera és essencial. El disseny per contracte és una forma de dissenyar formalment les interfícies dels components d’un software respecte de qui els crida, o clients. Aquest contracte té dues parts:
- Els requisits que demana el component als clients.
- Les promeses fetes pel component als clients.
Resumint, si el client compleix els requisits, el component promet complir el contracte que defineix.
Si canviem un contracte, volem que els clients no quedin afectats. Per assegurar-nos utilitzem la frase “no requerir més ni prometre menys”: si el canvi no requereix més dels clients ni promet menys, la nova especificació es compatible i no trencarà el funcionament del client.
Algunes pràctiques per a seguir el disseny per contracte:
-
Documentar el contracte amb comentaris, responent quins són els requisits i les promeses que es fan. En resum, explicant exactament què es fa sense haver de saber com. A Java es fa al javadoc de la classe i dels mètodes públics.
-
Validar arguments dels mètodes i constructors públics. És raonable no fer-ho amb els privats, ja que només es criden per la mateixa classe. D’això també se’n diu precondicions. A Java, aquesta validació sol produir excepcions unchecked com IllegalArgumentException, NullPointerException o IllegalStateException que no cal obligatòriament documentar.
-
Validar les promeses que fa un mètode al seu client. D’això també se’n diu postcondicions. Per exemple, comprovar valors, tipus de retorn, errors i excepcions que es produeixen. Es pot fer just abans d’acabar un mètode públic.
-
Validar l’estat de l’objecte. D’això també se’n diu invariants de classe. Implica mantenir una sèrie de condicions sobre l’estat entre les crides a mètodes públics. Si l’objecte és immutable, només cal validar l’estat al constructor. Si és mutable, a cada mètode que canvia l’estat.
-
Opcionalment, aspectes sobre el rendiment (temps i espai).
Estils d’arquitectura
L’arquitectura d’una solució pot ser monolítica o distribuïda. La distribuïda sol basar-se en client-servidor, tot i que també tenim la solució peer-to-peer.
A continuació revisarem alguns dels estils d’arquitectura.
- Multicapa (multitier). Basada en la client-servidor, estableix una sèrie de capes independents que permeten desenvolupar cada capa de forma prou independentment. Les capes habituals són:
- Presentació
- Aplicació
- Dades
- Microkernel. Es tracta d’una arquitectura amb una funcionalitat central que permet afegir funcionalitats addicionals en forma de plug-ins.
- Orientada a esdeveniments (event-driven). Basada en la comunicació asíncrona d’esdeveniments. Associada al concepte de “event-broker”, un middleware que s’encarrega d’encaminar esdeveniments entre sistemes que implementen el patró productor-consumidor. Els missatges poden ser guardats a cues fins a ser processats.
- Microserveis. Es traca d’un ecosistema de serveis d’un propòsit senzill que es comuniquen mitjançant un gateway d’APIs. Cada servei utilitza les seves pròpies dades.
Patrons d’arquitectura
L’arquitectura hexagonal és un model arquitectural que permet separat el core de negoci (o domini) de la infraestructura (UI, base de dades, APIs, frameworks, etc.). Ho fa proposant els adaptadors i els ports.
Els adaptadors fan la interacció de la nostra aplicació cap al món. Tenim dos tipus:
- Els primaris: puts d’entrada de l’aplicació, operats principalment pels usuaris (UI, API Rest, CLIs).
- Els secundaris: actors secundaris com les bases de dades o serveis de tercers.
Si volem seguir el principi d’inversió de dependència, les capes internes no poden dependre de les externes. O sigui, les dependències han de ser de fora a dins: dels adaptadors cap al core.
Els ports són fronteres abstractes a l’exterior, per exemple, utilitzant interfícies, i els adaptadors són implementacions concretes dels ports, habitualment injectades:
- Els adaptadors primaris depenen de ports d’entrada, i dirigeixen l’aplicació (API, GUI, CLI).
- Els adaptadors secundaris implementen ports de sortida, i són dirigits per l’aplicació (BBDD, API clients).
DDD (Domain-drive design) és una estratègia de disseny de software focalitzat en el modelatge del software, i que té l’objectiu de replicar una àrea temàtica o domini (domain) gràcies als coneixements dels experts d’aquesta àrea.
Dins del DDD hi trobem diversos tipus de models, com les entitats, que tenen identitat, o les value objects, objectes immutables sense identitat. També hi ha aggregates, que són altres models dirigits per una entitat arrel, i que són unitats de consistència, concurrència i distribució.
De vegades hem de comunicar el nostre domini amb altres externs, que provoquen una dependència externa. És una mala estratègia permetre que es filtri aquest model extern cap al nostre, i la forma d’evitar-ho és construir una capa anti-corrupció. La idea tècnica es pot implementar utilitzant els patrons Facade i Adapter.
L’arquitectura clean afegeix el concepte d’entitats i casos d’ús al core. Les dues es basen en el DDD. Les capes que defineix, de més interna a més externa són:
- Les entitats: objectes de negoci amb comportament
- Els casos d’ús (use cases): mapejat de la funcionalitat de les user stories
- Els adaptadors de interfícies (controladors, vistes, presentadors)
- Els frameworks i eines (BBDD, serveis de tercers)
El principi de la regla de dependència diu que cap capa interna ha de dependre d’una externa. Això es manifesta de la següent forma:
- Sense dependències:
- Les entitats no depenen de ningú.
- Dependències cap a dins:
- Els casos d’ús depenen de les entitats, la capa més interna.
- Els adaptadors d’entrada depenen dels casos d’ús.
- Dependències cap a fora, però abstractes (inversió de dependència):
- Els casos d’ús depenen dels adaptadors de sortida. Per exemple, per fer persistència.

El diagrama mostra una arquitectura hexagonal amb conceptes DDD (clean). Les fletxes negres indiquen dependència d’una interfície i les blanques, implementació.
Cal no confondre la direcció de les dependències amb el flux de control. Per exemple, un flux de control d’una aplicació MVP podria ser controlador => cas d'ús => entitats => cas d'ús => presentador.
Finalment, cal dir que el cablejat dels components de l’arquitectura es fa des de fora de l’aplicació, creant les dependències, connectant-les i iniciant l’aplicació. El patró es diu composition root. Aquí sol utilitzar-se la injecció de constructor i, si hi ha contenidors que fan inversió de control, és l’únic lloc on haurien d’aparèixer.
Diagrames d’arquitectura
El model C4 és un model que permet visualitzar l’arquitectura d’una solució. Descriu quatre nivells de diagrames, de més generals a més concrets:
- De context de sistema: mostra el sistema software i el seu context al voltant.
- De contenidor: mostra els contenidors dins d’un sistema software i com es relacionen.
- De component: mostra els components d’un contenidor i les seves interaccions.
- De codi: mostra la implementació del codi amb diagrames UML, diagrames ER o similars.
Hi ha quatre abstraccions que poden aparèixer en el nostre diagrama:
- La persona: actors o rols d’un sistema software.
- El sistema software: el sistema que estàs modelant, i que es relaciona amb altres sistemes externs.
- Un contenidor: una aplicació o un magatzem de dades. Per exemple:
- Un backend web
- Un frontend web
- Una aplicació desktop (client)
- Una app mòbil
- Una aplicació de consola o script
- Una funció serverless (cloud)
- Una base de dades
- Un magatzem al núvol
- El sistema d’arxius
- Un component: grup de funcionalitats encapsulada darrere d’una interfície (o contracte). Tots els components d’un contenidor solen executar-se en el mateix espai de processos o màquina virtual.
Alguns consells per a dibuixar diagrames:
- No hi ha una forma estàndard de fer-ho.
- Han de reflectir la realitat. Abstraccions primer, notació després.
- Conté blocs per als diferents components (SoC). Alguna cosa modular amb una interfície/fronteres. Els components contenen codi.
- Agrupar els components que treballen junts amb caixes (contenidors): DB schema, app mòbil, backend server-side app, console app, windows service.
- Ha d’incloure les dades, la lògica de negoci i la interfície d’usuari.
- Fletxes sempre direccionals per indicar la direcció del flux (petició) amb descripció.
- No cal afegir respostes (fletxes de tornada) dient OK, només qui les origina.
- Ser consistent en forma i color, evitar acrònims no coneguts.
REST APIs
Criteris de disseny
- Identificar els recursos que són part de l’API i els seus IDs.
- Definir l’URI del recurs, o també anomenades endpoints. Utilitzen noms (no verbs).
- Quan es retorna un sol recurs es retorna informació completa. Quan es retornen col·leccions se sol reduir la informació a l’estrictament necessària.
- Assignar els mètodes adequats.
Mètodes
El content type a utilitzar amb les peticions és “application/json”. Usos dels mètodes HTTP:
- GET recupera una representació del recurs a l’URI especificat.
- Si es troba, 200 (Ok). El cos del missatge de resposta conté els detalls del recurs sol·licitat.
- Si no es troba, 404 (Not Found).
- També es pot retornar 204 (No Content) si ha anat bé, però no es retorna cap contingut.
- Si les dades enviades no són vàlides, 400 (Bad Request). El cos pot incloure informació addicional sobre el problema.
- POST crea un recurs nou a l’URI especificat. El cos del missatge de sol·licitud proporciona els detalls del nou recurs. Tingueu en compte que POST també es pot utilitzar per activar operacions que en realitat no creen recursos.
- Si es crea un nou recurs, 201 (Created). El recurs pot retornar-se al cos.
- Si es fa algun procés, però no es crea res, 200 (Ok). El cos pot incloure el resultat de l’operació. Alternativament, si no hi ha resultat, es pot retornar 204 (No Content) sense cos.
- Si les dades enviades no són vàlides, 400 (Bad Request). El cos pot incloure informació addicional sobre el problema.
- PUT crea o substitueix el recurs a l’URI especificat. El cos del missatge de sol·licitud especifica el recurs que s’ha de crear o actualitzar.
- Si es crea un nou recurs, 201 (Created).
- Si s’actualitza, 200 (Ok) o 204 (No Content).
- Si no es possible l’actualització, 409 (Conflict).
- PATCH realitza una actualització parcial d’un recurs. El cos de la sol·licitud especifica el conjunt de canvis que cal aplicar al recurs. Les respostes podrien ser com les de PUT.
- DELETE elimina el recurs a l’URI especificat.
- Si funciona, 204 (No Content), sense retornar cap informació.
- Si es retorna alguna informació també es pot utilitzar 200 (Ok).
- Si no existeix, 404 (Not Found).
Aquests són alguns exemples d’endpoints i com se solen utilitzar segons els mètodes:
| Recurs | POST | GET | PUT | DELETE |
|---|---|---|---|---|
| /customers | Crear un nou client | Obtenir tots els clients | Actualitzar tots els clients | Esborrar tots els clients |
| /customers/1 | N/A | Obtenir els detalls del client 1 | Actualitzar els detalls del client 1, si existeix | Esborrar client 1 |
| /customers/1/orders | Crear una nova comanda per al client 1 | Obtenir totes les comandes del client 1 | Actualitzar totes les comandes del client 1 | Esborrar totes les comandes del client 1 |
Bones pràctiques
- Utilitzar JSON com a format per a enviar i rebre dades (cos).
- Utilitzar noms en lloc de verbs per als endpoints.
- Els endpoints de col·leccions s’han d’anomenar amb noms plurals.
- No utilitzar verbs a la URI. P. ex. si cal fer una acció, fer-ho sobre un nom amb un paràmetre ‘action’ en la query.
- Envia codis d’estat per a gestionar els errors.
- Utilitza filtres, ordenació i paginació a les dades.
- Genera una bona documentació de l’API (OpenAPI).
Filtres, ordenació i paginació
Quan s’exposa un conjunt de recursos a un endpoint, cal evitar retornar una quantitat molt gran de dades. L’API hauria de permetre especificar filtres a l’URI: recursos?filtre1=valor1&filtre2=valor2…. També caldria especificar a l’URI com obtenir només una part dels resultats, quan poden ser molts.
L’ordenació també es pot realitzar amb un paràmetre del tipus recursos?order_by=criteri.
Cal utilitzar paginació sempre que una col·lecció de recursos pugui ser gran perquè pugui créixer sense límit. Es pot fer principalment de tres formes:
- offset:
recursos?limit=nombre&offset=nombrepermet utilitzar els paràmetres SQL per limitar els resultats. L’opció més senzilla, però poc òptima per a offsets alts: cal obtenir tots els registres anteriors en la query. - keyset: filtra pel valor d’un camp que defineix l’ordre. Per exemple, la data de creació. Podem utilitzar
recursos?limit=nombre&from_date=data. - seek: similar a l’anterior, però utilitzant una primary key. Podem utilitzar
recursos?limit=nombre&after_id=id.
Per a la paginació, pots utilitzar un valor de límit per defecte si no es diu res. Per exemple, 20. No s’hauria de permetre un valor qualsevol per a aquest paràmetre (ha d’estat limitat). També pots retornar informació al cos de la resposta que pugui ajudar al client a gestionar el resultat.
Referències
- The C4 model for visualising software architecture
- PlantUML
- UML diagrams with plantUML
- Inversion of Control Containers and the Dependency Injection pattern
- Best Practices for Designing and Implementing a Library in Java
- Dependency injection
- Software Architecture Boundaries
- Acyclic Dependencies Principle
- Top 20 Design Heuristics
- Design Principles (Bob Martin)
- Listing of Arthur Riel’s heuristics
- Design by Contract
- Hexagonal architecture
- The Clean Architecture
- Hexagonal Architecture with Java and Spring
- Domain-centric Architectures (Clean and Hexagonal) for Dummies
- RESTful web API design
- REST API Design: Filtering, Sorting, and Pagination
- What is REST
- OpenAPI
- Architectural boundaries
- Simpler Encapsulation with Immutability
- Effective Java by Joshua Bloch
POO
Conceptes de POO
Abstracció
L’abstracció amaga la complexitat a l’usuari i només mostra la informació rellevant. Permet centrar-nos en el “què” en lloc del “com”. Els detalls d’un mètode abstracte són implementats de forma separa per cada classe.
Objectius: reusabilitat de codi, flexibilitat d’implementació, herència múltiple.
Java: classes abstractes (abstracció parcial) i interfícies (abstracció total).
Sintaxi: abstract (classes abstractes) i interface, implements (interfícies).
Encapsulació
L’encapsulació lliga les dades i els mètodes relacionats dins d’una classe. També protegeix les dades (estat) fent els camps privats i donant accés a ells només mitjançant els mètodes relacionats que implementen les regles de negoci i invariants.
Objectius: protecció de dades (estat), llegibilitat del codi.
Java: camps privats i mètodes públics (només els exposats).
Sintaxi: private, setX(), getX().
Herència
Amb l’objectiu de reusar codi, l’herència permet que una classe fill hereti les característiques (camps i mètodes) d’una altra classe pare. La relació es diu is-a. L’herència s’utilitza quan sabem que:
- Les dues classes es troben en el mateix domini lògic.
- La superclasse és un contracte que no canviarà (interfície o classe abstracta).
- Les millores realitzades per la subclasse són principalment additives.
L’alternativa és la composició.
Objectius: reusabilitat de codi, llegibilitat del codi.
Java: Classe pare i class fill. Alternativa vàlida: patrons de composició i delegació.
Sintaxi: extends.
Composició
La composició és una alternativa de l’herència per a reusar codi. La relació es diu has-a. Es pot aconseguir utilitzant instàncies d’altres objectes. És una tècnica preferible a la herència, ja que redueix l’acoblament del codi. Un tipus específic de composició és l’agregació, que no implica la propietat dels objectes interns i, per tant, tampoc la destrucció d’aquests quan l’objecte contenidor es destrueix.
Objectius: reusabilitat de codi.
Java: variables d’instància d’altres classes.
Sintaxi: private ClassName instanceName.
Polimorfisme
El polimorfisme permet substituir i estendre la funcionalitat d’objectes que tenen una mateixa interfície per uns altres durant el temps d’execució (dynamic binding).
Objectius: llegibilitat del codi, flexibilitat del codi.
Java: sobreescriptura de mètode (dinàmic), menys rellevant: sobrecàrrega de mètode (estàtic).
Sintaxi: myMethod() myMethod(int x) myMethod(int x, String y) (estàtic) i ParentClass.myMethod() ChildClass.myMethod() (dinàmic).
Relacions entre objectes
Aquests són els tipus de relacions comunes entre objectes:
- Associació: un objecte fa referència a un altre.
- Dependència: un objecte rep una referència en una de les seves operacions.
- Agregació: un objecte fa referència a diversos objectes.
- Composició: un objecte crea diversos objectes.
Aquest és un diagrama que les mostra.
- Herència de Number i implementació de Comparable
- Associació entre Child1 i Parent1
- Associació navegable des de Child2 cap a Parent2
- Agregació: Wheel pot existir sense Car
- Composició: Heart no pot existir sense Human
- Dependència: Object1 usa l’Object2
Tipus d’objectes
Un objecte és una entitat que pot integrar comportament i estat. La pràctica més acceptada és la d’encapsular l’estat darrere del comportament, perquè formen part de la implementació, que és convenient amagar.
Una primera classificació dels objectes es pot fer en funció de dues característiques: si tenen estat (stateful) o no (stateless), i en el cas que en tinguin, si són immutables o no (mutables):
- Un objecte té estat si el seu comportament depèn d’interaccions prèvies. Els objectes sense estat tenen un comportament esperable que només depèn dels paràmetres de les crides, com passa a la programació funcional.
- Un objecte immutable té un estat que no pot canviar-se després de ser creat. Permeten escriure codi més fàcil d’entendre i segurs quan hi ha concurrència.
Segons el seu ús de les dades, podem fer una classificació dels tipus d’objectes que podem trobar més freqüentment a Java:
- Abstract Data Types (ADT): són objectes amb comportament i estat encapsulat, i habitualment mutables. Cal definir detalladament el seu comportament, i evitar retornar referències a objectes mutables del seu estat.
- Estructures de dades: són objectes amb només estat, i poden ser immutables. Permeten transferir dades o representar un estat.
- Serveis: són objectes amb només comportament que poden operar sobre dades. També es poden anomenar utilitats o helpers.
Aquestes són propietats a l’hora de classificar classes d’objectes:
| Propietat | Valors | Explicació |
|---|---|---|
| State | Stateless / Stateful | Si té estat o no |
| Mutability | Mutable / Immutable | Si és modificable un cop creat o no |
| Lifecycle | One-shot / Reusable | Si es pot reutilitzar un cop feta la seva funció |
| Concurrency | Thread-safe / Not | Si es pot compartir entre fils d’execució |
| Timing | Synchronous / Asynchronous | Si s’executa immediatament o ho fa després concurrentment |
| Purity | Pure / Impure | Si no té efectes secundaris i retorna sempre el mateix resultat per als mateixos paràmetres |
| Ownership | Owns Resources / Not | Si és responsable de netejar recursos quan acaba |
| Extensibility | Open / Closed | Si el disseny permet o no estendre la classe |
Patrons de disseny
Un patró de disseny és una solució general a un problema comú i recurrent en el disseny de programari. Un patró de disseny no és un disseny acabat que es pot transformar directament en codi; és una descripció o plantilla per resoldre un problema que es pot utilitzar en moltes situacions diferents.
Tenim algunes categories generals:
- De comportament: identifiquen patrons de comunicació entre objectes.
- Estructurals: faciliten el disseny quan s’han d’establir relacions entre entitats.
- Creacionals: relacionats amb mecanismes de creació d’objectes de la forma més adient per cada cas.
- Concurrència: tracten el paradigma de programació multifil.
A continuació es mostren alguns patrons importants.
Patrons de comportament
Command
El patró command encapsula una sol·licitud com a objecte, de manera que us permetrà parametrizar altres objectes amb diferents peticions, cues o peticions de registre i donar suport a operacions reversibles.
Tenim dos actors principals: el Client crea el ConcreteCommand i assigna el seu Receiver. L’Invoker té Commands que pot executar.
Iterator
El patró iterator proporciona una manera d’accedir als elements d’un objecte agregat seqüencialment sense exposar la seva representació subjacent.
Exemples: java.util.Iterator i java.util.Enumeration
Observer
El patró observer defineix una dependència entre molts objectes de manera que quan un objecte canvia d’estat, tots els seus dependents són notificats i actualitzats automàticament.
Exemples: java.util.EventListener
State
El patró state permet a un objecte alterar el seu comportament quan canvia el seu estat intern. L’objecte semblarà canviar de classe.
El Context pot tenir un nombre de States. Quan cridem request(), el que fem es cridar el handle corresponent del State actual. Un Client no coneix el funcionament intern dels States.
Strategy
El patró strategy s’utilitza quan tenim diversos algorismes per a una tasca específica i el client decideix que s’utilitzi la implementació real en temps d’execució.
És molt similar a State, però és el Client qui habitualment escull la Strategy. Exemples: Collections.sort() amb el paràmetre Comparator
Dependency Injection
El patró de Dependency Injection permet a un objecte rebre les instàncies d’altres objectes de que depèn. Això permet que el client no s’hagi de referir a instàncies concretes, seguint el principi d’inversió de dependència. S’utilitza per implementar el principi de la inversió de control (IoC) mitjançant un contenidor.
Hi ha tres tipus d’injecció: Constructor, Setter i Interface.
- Constructor: li diem al contenidor quina implementació s’utilitza per als paràmetres del constructor, que poden ser interfícies.
- Setter: li diem al contenidor quines propietats (amb setters) cal instanciar d’un cert objecte.
- Interface: li diem al contenidor que el nostre client rebrà les dependències mitjançant un mètode (del tipus setService).
Per utilitzar-los, cal configurar el contenidor. Es pot fer programàticament o mitjançant un arxiu de configuració.
L’objecte que injecta les dependències es diu injector, i els objectes de que depèn solen ser serveis.
Service Locator
El Service Locator és una alternativa al Dependency Injector, amb la particularitat que el codi client té una dependència del contenidor, que utilitza per localitzar tots els serveis que necessita. És a dir, no es produeix la injecció automàtica.
No es tracta d’una Factory, ja que no crea els serveis cada cop, només si cal. Es tracta més bé d’un registre.
Template method
El patró template method defineix l’esquelet d’un algorisme d’un mètode, diferint alguns passos a subclasses. Aquest patró permet que les subclasses redefineixin certs passos d’un algorisme sense canviar l’estructura de l’algoritme.
El templateMethod fa ús del subMethod. Exemple: construcció d’una casa de fusta o de vidre
Patrons estructurals
Adapter
El patró adapter converteix la interfície d’una classe en una altra interfície que els clients esperen. L’adaptador permet que les classes treballin conjuntament, tot i tenir interfícies incompatibles.
El ConcreteAdapter està composat amb l’Adaptee, que li permet una operació addicional: adaptedOperation. Exemple: un capità que només pot utilitzar barques de rem i no pot navegar
Composite
El patró composite permet compondre objectes en estructures d’arbre per representar jerarquies parcials. Composite permet als clients tractar objectes individuals i composicions d’objectes de manera uniforme.
Tant els objectes individuals com els composats poden ser tractats igual amb operation.
Decorator
El patró decorator atribueix dinàmicament responsabilitats addicionals a un objecte. Els decorators proporcionen una alternativa flexible a la subclasse per ampliar la funcionalitat.
Podem afegir un comportament al ConcreteDecorator. Exemple: trol decorat amb una porra
Facade
El patró facade proporciona una interfície unificada i més senzilla a un conjunt d’interfícies d’un subsistema. La façana defineix una interfície de més alt nivell que facilita la utilització del subsistema, seguint el principi del “Least knowledge”.
Exemple: treballadors de la mina d’or
Proxy
El patró de proxy proporciona un substitut per a un altre objecte per controlar-ne l’accés.
Qualsevol Client pot tractar el Proxy com el RealSubject, ja que implementen Subject. Exemple: els tres mags que entren a la torre
Patrons creacionals
Separa la construcció d’un objecte complex de la seva representació, de forma que el mateix procés de construcció pot generar diferents representacions.
Builder
Factory method
Factory method defineix una interfície per crear un objecte, però permet que les subclasses decideixin quina classe s’inicia. Permet diferir la instància de classe a subclasses.
El mètode factoryMethod és abstracte, i s’encarrega de crear Products. Permet seguir el principi de “Dependency inversion”: evitar dependències de tipus concrets. Exemple: el regne que necessita tres objectes temàtics
Singleton
El patró de Singleton assegura que una classe només té una instància i li proporciona un punt d’accés global.
Exemple: només pot haver una torre d’ivori
Referències
- Programming paradigm
- Composition vs. Inheritance: How to Choose?
- Java Design Patterns
- Difference between Association, Aggregation and Composition in UML, Java and Object Oriented Programming
- PlantUML Design Patterns
- Composition vs Inheritance
Workflow Git
- Conceptes
- Configuració inicial
- Flux de treball
- Escenari base
- Escenari amb conflicte
- Estratègies de branques
- Bones pràctiques
- Git i CI/CD
- Regles d’or del treball en equip
- FAQ
- Com crear un repositori a partir d’una carpeta existent?
- Quina diferència hi ha entre
git pulligit fetch+git merge? - Com s’associa una branca local amb una branca remota?
- He fet commit d’un arxiu que no hauria d’haver afegit. Com ho desfaig?
- Com puc veure els canvis que he fet abans de fer commit?
- He modificat arxius però vull descartar tots els canvis i tornar a l’últim commit
- Com puc desfer o corregir l’últim commit (si no he fet push)?
- Com puc tornar el repositori a l’estat que hi ha remotament?
- Referències
Aquesta entrada descriu un flux de treball amb git per a equips de desenvolupament, des de la línia de comanda. El focus principal és en equips petits (2-3 persones) amb un enfocament trunk-based (treballant directament sobre la branca principal), però també es presenten alternatives com les feature branches per a equips més grans o projectes amb més complexitat.
Conceptes
En local
git és una eina que permet treballar amb repositoris de codi locals i remots.
Els canvis sobre els arxius d’un repositori s’agrupen en commits. Un commit és l’acte d’emmagatzemar un conjunt de canvis al repositori.
En l’àmbit local, tenim tres espais:
- El working directory és el lloc on tens el teu codi. A l’arrel del teu working directory tindràs sempre una carpeta anomenada .git on es guarden els altres dos espais.
- El staging area és una capsa on pots ficar i treure arxius. Un commit estarà format per tots els arxius ficats en aquesta àrea, i s’identifica amb un hash o resum. Quan es fa el commit, es buida.
- El repositori és el lloc on s’emmagatzemen els commits d’arxius provenents del stagging area. Podem revisar i recuperar qualsevol arxiu de qualsevol commit del passat. El commit actual d’un repositori es diu HEAD.
Un repositori pot tenir branques (branches). Les branques permeten divergir de la línia principal de desenvolupament i fer feina sense afectar-la. En els exemples d’aquest document treballarem directament sobre la branca principal per simplificar, però més endavant es presenten les estratègies de branques disponibles. Cal saber que master és el nom de la branca que git crea per defecte quan es crea un repositori.
En remot
Opcionalment, podem tenir repositoris remots, i comunicar-nos per pujar o baixar coses. Un repositori remot és com un de local, però no té working directory. Se’n diu “bare”.
Ens interessa tenir-ne de remots per poder tenir un lloc on compartim el codi amb la resta de membres del grup. El flux de treball serà treballar en local i compartir en remot la feina, un cop la tenim enllestida.
A un repositori local podem emparellar un de remot. El nom que git dona al principal repositori remot és origin. Un cop els hem emparellat, el codi NO se sincronitza automàticament. Tenim disponibles una sèrie d’operacions:
- fetch: guarda en local els canvis remots (sense integrar-los).
- merge: barreja els canvis remots que tenim en local amb els locals.
- pull: és el mateix que fer un fetch i després un merge.
- push: puja tots els canvis locals al repositori remot.
Configuració inicial
Eina git
Instal·la la teva eina git de línia de comanda al teu sistema operatiu.
Intenta executar-la:
git --version
Crear el repositori
Primer, has de crear un repositori buit a github o a gitlab.
Quan l’hagis creat, pots obtenir un URL del tipus:
https://github.com/usuari/repositori.git o bé https://gitlab.com/usuari/repositori.git.
Configuració
Les següents tres comandes són interessants per treballar: les dues primeres, calen per indicar el teu usuari i correu que es guarda a l’activitat del repositori. El tercer serveix per guardar les credencials el primer cop que s’introdueixen. Compte: es guarden en text pla a $HOME/.git-credentials.
git config --global user.email “elteu@correu.com”
git config --global user.name “elteunom”
git config --global credential.helper store
El flag –global indica que els canvis apliquen a tots els repositoris. Si no s’indica, només aplica al repositori en què ens trobem.
També es pot configurar credential.helper per utilitzar una cache (900 segons per defecte):
git config --global credential.helper cache
Si es volen ignorar els canvis fets al mode dels arxius, es pot fer:
git config --global core.filemode false
La comanda per esborrar una entrada és:
git config --global --unset <key>
Clonar el repositori
A partir d’ara es parla de github, però les comandes són exactament les mateixes canviant l’URL pel de gitlab.
Clonarem el repositori buit que hem creat a github:
1$ git clone https://github.com/usuari/repositori.git
Això crea una carpeta “repositori” amb el working directory i la carpeta .git a dins.
Per mostrar l’estat:
1$ git status
On branch master
No commits yet
nothing to commit (create/copy files and use “git add” to track)
També pots mirar l’aparellament amb el repositori remot:
1$ git remote -v
origin https://github.com/usuari/repositori.git (fetch)
origin https://github.com/usuari/repositori.git (push)
Pots veure les branques locals i remotes així:
1$ git branch
* master
1$ git branch -r
origin/HEAD -> origin/master
origin/master
Flux de treball
Aquest és el flux de treball de referència que es detalla en els escenaris següents. Serveix com a guia ràpida de la sessió de treball habitual amb git: sincronitzar, resoldre conflictes si n’hi ha, treballar en local i pujar els canvis.
- Obtenir canvis remots, en dos passos:
- Obtenir-los amb git fetch
- Barrejar-los amb git merge
- Si el merge genera conflicte:
- Editar arxius conflictius
- Fer git add de les solucions
- Fer git commit
- Fer canvis en local:
- Modificar els arxius del working directory
- Afegir-los al staging area (git add)
- Fer commit (git commit)
- Pujar canvis locals:
- Fer git push
Escenari base
Reproduirem l’escenari base, amb dos usuaris i un repositori remot compartit. Els dos usuaris fan canvis en local i els sincronitzen amb el repositori remot.
Afegir contingut
Per afegir contingut, cal preparar el commit. Primer, crea o copia al working directory tot el contingut que vulguis.
Imaginem que afegim un arxiu així:
echo "Hola, món!" > arxiu.txt
Si mostres l’estat:
1$ git status
No commits yet
Untracked files:
(use "git add <file>..." to include in what will be committed)
arxiu.txt
nothing added to commit but untracked files present (use "git add" to track)
Els missatges expliquen que tenim un arxiu fora del control del repositori (untracked). Per afegir-lo:
1$ git add arxiu.txt
1$ git status
On branch master
No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: arxiu.txt
L’estat mostra que l’arxiu és al staging area (Changes to be committed). Ara ja podem crear el commit:
1$ git commit -m "primer commit"
[master (root-commit) 17466a8] primer commit
1 file changed, 1 insertion(+)
create mode 100644 arxiu.txt
1$ git status
On branch master
Your branch is based on 'origin/master', but the upstream is gone.
(use "git branch --unset-upstream" to fixup)
nothing to commit, working tree clean
També pots mirar el log, el lloc on es guarden els canvis del repositori:
1$ git log
commit 17466a86c10203150c8502e3aaedb8066c9d9b67 (HEAD -> master)
Author: elteunom <elteu@correu.com>
Date: Sun Apr 26 19:39:54 2020 +0200
primer commit
També hi ha un format en una línia d’aquesta comanda:
1$ git log --graph --oneline
* 17466a8 (HEAD -> master) primer commit
El log mostra el commit “17466a8” amb el seu missatge. HEAD -> master indica que és el commit actual de la branca master.
Pujar contingut
Cal fer un push:
1$ git push
Enumerating objects: 3, done.
Counting objects: 100% (3/3), done.
Writing objects: 100% (3/3), 216 bytes | 216.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To https://github.com/usuari/repositori.git
* [new branch] master -> master
Nou estat:
1$ git status
On branch master
Your branch is up to date with 'origin/master'.
nothing to commit, working tree clean
Ara l’estat confirma que estem sincronitzats amb origin/master (el repositori remot). Si mirem el log:
1$ git log --graph --oneline
* 17466a8 (HEAD -> master, origin/master) primer commit
Ara apareix origin/master al costat del commit, confirmant que tant el repositori local com el remot apunten al mateix commit.
Treballar amb un segon usuari
Simularem que tenim un segon usuari amb un altre repositori. Per simplificar, els dos usuaris poden compartir credencials de github. Alternativament (recomanable), crea tants usuaris com calgui, i fes que siguin col.laboradors del projecte. Això es pot fer tant a github com a gitlab:
- github: cal afegir un col·laborador des del projecte > Settings > Manage access > Invite a collaborator.
- gitlab: cal afegir un membre des del projecte > Settings > Members > Invite member. Selecciona “mantainer” com a perfil.
Creem un segon workspace directory. Per distingir els dos, tindrem dos prompts diferents: 1$ i 2$.
2$ git clone https://github.com/usuari/repositori.git
Cloning into 'repositori'...
remote: Enumerating objects: 3, done.
remote: Counting objects: 100% (3/3), done.
remote: Total 3 (delta 0), reused 3 (delta 0), pack-reused 0
Unpacking objects: 100% (3/3), done.
Ara, modificarem l’arxiu i mirem l’estat:
2$ echo "Com ba tot?" >> arxiu.txt
2$ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: arxiu.txt
no changes added to commit (use "git add" and/or "git commit -a")
Ens diu que hi ha un arxiu modificat (modified), però no està al staging area.
Ens hem equivocat! Volíem escriure “Com va tot?”. Podríem editar l’arxiu un altre cop i esborrar la nova línia, però aprofitarem per recuperar l’arxiu abans de fer la modificació. Com que no hem fet el commit, es pot recuperar així:
2$ git reset --hard
HEAD is now at 17466a8 primer commit
2$ echo "Com va tot?" >> arxiu.txt
Ara, afegim l’arxiu al staging area i fem el commit:
2$ git add arxiu.txt
2$ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: arxiu.txt
2$ git commit -m "afegim pregunta"
[master b475802] afegim pregunta
1 file changed, 1 insertion(+)
2$ git status
On branch master
Your branch is ahead of 'origin/master' by 1 commit.
(use "git push" to publish your local commits)
nothing to commit, working tree clean
Ara, afegim els canvis al repositori remot:
2$ git push
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Writing objects: 100% (3/3), 263 bytes | 263.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To https://github.com/usuari/repositori.git
17466a8..b475802 master -> master
2$ git status
On branch master
Your branch is up to date with 'origin/master'.
nothing to commit, working tree clean
Ja tenim tots els canvis en remot. Mirem el log:
2$ git log --graph --oneline
* b475802 (HEAD -> master, origin/master, origin/HEAD) afegim pregunta
* 17466a8 primer commit
Com es veu, l’últim commit (b475802: “afegim pregunta”) es mostra com l’actual.
Rebre els canvis al primer usuari
Ara retornem al primer usuari (1$). Si mirem l’estat i el log:
1$ git status
On branch master
Your branch is up to date with 'origin/master'.
nothing to commit, working tree clean
1$ git log --graph --oneline
* 17466a8 (HEAD -> master, origin/master) primer commit
Com es pot veure, l’estat diu que està actualitzat amb origin/master (repositori remot), i al log no hi ha el nou commit que s’ha pujat al repositori remot (“afegim pregunta”).
Per poder veure’l, cal baixar-se els canvis del remot. Això es pot fer amb un fetch:
1$ git fetch
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Total 3 (delta 0), reused 3 (delta 0), pack-reused 0
Unpacking objects: 100% (3/3), done.
From https://github.com/usuari/repositori
17466a8..b475802 master -> origin/master
Si es mira l’estat i el log:
1$ git status
On branch master
Your branch is behind 'origin/master' by 1 commit, and can be fast-forwarded.
(use "git pull" to update your local branch)
nothing to commit, working tree clean
1$ git log --graph --oneline
* 17466a8 (HEAD -> master) primer commit
El log no ha canviat, perquè fetch no integra els canvis al repositori, però l’estat sí: ara ens diu que estem per darrere d’origin/master, i que hauríem de fer un git pull. Com que un pull és un fetch + merge, farem només el merge.
El merge intentarà barrejar automàticament el contingut remot recuperat i el que tenim al working directory.
1$ git merge
Updating 17466a8..b475802
Fast-forward
arxiu.txt | 1 +
1 file changed, 1 insertion(+)
El merge ha funcionat: ha afegit una nova línia. Com es veu, aquesta operació és immediata: no necessita anar al repositori remot. L’arxiu s’ha actualitzat, i l’estat i el log estan igualats amb els de l’usuari 2:
1$ cat arxiu.txt
Hola, món!
Com va tot?
1$ git status
On branch master
Your branch is up to date with 'origin/master'.
nothing to commit, working tree clean
1$ git log --graph --oneline
* b475802 (HEAD -> master, origin/master) afegim pregunta
* 17466a8 primer commit
Etiquetes
Les etiquetes (o tags) és una forma senzilla d’identificar un cert commit o estat dins del repositori. Es poden posar locals i pujar-les en remot. A github, quan es pugen en remot, s’associen a una release que permet descarregar un arxiu empaquetat. A gitlab també es pot fer, però la secció es diu “tags”.
Per afegir un tag al commit actual i mostrar-lo:
1$ git tag v1.0
1$ git tag
v1.0
Per pujar una etiqueta:
1$ git push origin v1.0
Total 0 (delta 0), reused 0 (delta 0)
To https://github.com/usuari/repositori.git
* [new tag] v1.0 -> v1.0
Si volem veure les etiquetes des de l’altre repositori:
2$ git fetch
From https://github.com/usuari/repositori
* [new tag] v1.0 -> v1.0
2$ git tag
v1.0
Tags anotats
L’exemple anterior crea un tag lleuger (lightweight). Git també permet crear tags anotats, que inclouen un missatge descriptiu, l’autor i la data. Són més informatius i recomanables per a versions o fites del projecte:
1$ git tag -a v1.0 -m "Primera versió estable amb funcionalitat bàsica"
1$ git push origin v1.0
Un bon missatge de tag descriu el contingut del desplegament. Evita repetir el nom del tag com a missatge:
# Poc informatiu
git tag -a v1.0 -m "v1.0"
# Informatiu: descriu el contingut
git tag -a v1.0 -m "Primera versió: API REST amb endpoints /users i /products"
Convencions de noms per a tags
| Convenció | Exemple | Ús recomanat |
|---|---|---|
| Semantic versioning | v1.0.0, v1.1.0, v1.1.1 | Projectes amb API pública o versionat formal |
| Sprint-based | sprint-1, sprint-2 | Projectes acadèmics o amb sprints definits |
| Data-based | 2026-03-15 | Menys recomanat (no descriu el contingut) |
El semantic versioning (vMAJOR.MINOR.PATCH) és l’estàndard professional: incrementa MAJOR per canvis incompatibles, MINOR per noves funcionalitats, i PATCH per correccions.
Checkout: viatjar en el temps
El checkout ens permet recuperar qualsevol working directory per a un commit. És la veritable raó de ser dels repositoris: poder viatjar en el temps.
Per exemple, podem recuperar un arxiu concret d’un commit. De l’últim ( — significa que no indiquem el commit), o d’un concret:
1$ git checkout -- arxiu.txt
1$ git checkout 17466a8 arxiu.txt
Podem recuperar tot un commit, per exemple, el “primer commit”:
1$ git checkout 17466a8
Note: checking out '17466a8'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:
git checkout -b <new-branch-name>
HEAD is now at 17466a8 primer commit
1$ git status
HEAD detached at 17466a8
nothing to commit, working tree clean
1$ cat arxiu.txt
Hola, món!
Com es veu, tornem al contingut de l’arxiu abans del segon commit.
El problema d’aquesta comanda és que estem en estat “detached HEAD”: el HEAD apunta directament a un commit en lloc d’apuntar a una branca. Podem mirar i experimentar, però qualsevol commit que fem no pertanyerà a cap branca i es podria perdre.
Sempre podem retornar al master:
1$ git checkout master
Switched to branch 'master'
Your branch is up to date with 'origin/master'.
I si volem recuperar cert commit al master, per exemple, el “primer commit”:
1$ git reset --hard 17466a8
HEAD is now at 17466a8 primer commit
També podem fer referència a una etiqueta:
1$ git reset --hard v1.0
Si volem que el reset quedi al repositori remot:
1$ git push
To https://github.com/usuari/repositori.git
! [rejected] master -> master (non-fast-forward)
error: failed to push some refs to 'https://github.com/usuari/repositori.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
Això falla perquè no es pot fer un push d’un commit antic: git sempre comprova que siguin commits més nous. Podem ometre aquesta comprovació amb el paràmetre –force:
1$ git push --force
Total 0 (delta 0), reused 0 (delta 0)
To https://github.com/usuari/repositori.git
+ b475802...17466a8 master -> master (forced update)
Després de fer això, si anem a l’altre repositori i fem fetch:
2$ git fetch
From https://github.com/usuari/repositori
+ b475802...17466a8 master -> origin/master (forced update)
2$ git log --graph --oneline
* b475802 (HEAD -> master, tag: v1.0) afegim pregunta
* 17466a8 (origin/master) primer commit
2$ git status
On branch master
Your branch is ahead of 'origin/master' by 1 commit.
(use "git push" to publish your local commits)
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: arxiu.txt
no changes added to commit (use "git add" and/or "git commit -a")
Veiem que el repositori remot està al primer commit, però el local està al segon: ens diu “your branch is ahead”.
Això es pot resoldre canviant al commit remot en local:
2$ git reset --hard origin/master
2$ git status
On branch master
Your branch is up to date with 'origin/master'.
nothing to commit, working tree clean
2$ git log --graph --oneline
* 17466a8 (HEAD -> master, origin/master, origin/HEAD) primer commit
Comparar canvis amb diff
git diff permet comparar dos commits locals o remots. Per exemple, per comparar el HEAD amb el tag v1.0:
$ git diff v1.0
diff --git a/arxiu.txt b/arxiu.txt
index 0027e65..b4b62f7 100644
--- a/arxiu.txt
+++ b/arxiu.txt
@@ -1,2 +1,2 @@
Hola, món!
-Com va tot?
+segona línia 13
En aquest cas, ens diu que el canvi del tag v1.0 al HEAD és que s’ha esborrat una línia (“Com va tot?”) i s’ha afegit una altra (“segona línia 13”).
Si volem comparar dos commits, afegirem dos paràmetres. Per exemple, si hem fet prèviament un git fetch, podem utilitzar aquesta comanda per veure si el master local i el remot estan sincronitzats:
git diff master origin/master
Esborrar arxius del repositori
Per esborrar un arxiu o una carpeta del repositori:
git rm arxiu.txt
git rm --cached arxiu1.txt
git rm -r carpeta
L’opció --cached esborra l’arxiu del repositori però el manté al working directory. L’opció -r permet esborrar carpetes de forma recursiva. Després, cal fer git commit per confirmar l’eliminació.
Gitignore
Alguns tipus de fitxers no haurien de ser part del repositori de codi, i es poden indicar afegint un patró a l’arxiu .gitignore que hi ha a les carpetes. En general, seria millor no afegir certs tipus d’arxius:
- cachés de dependències, com els continguts de
/node_moduleso/packages - codi compilat, com
.o,.pyc, i.class - carpetes de sortida de compilació, com
/bin,/out, o/target - arxius generats en temps d’execució com
.log,.lock, o.tmp - arxius amagats del sistema, com
.DS_StoreoThumbs.db - arxius de configuració personal dels IDE, com
.idea/workspace.xml
Escenari amb conflicte
Què passa quan dos desenvolupadors modifiquen la mateixa línia d’un arxiu? Git no pot decidir quina versió és correcta, i genera un conflicte que cal resoldre manualment. Vegem-ho pas a pas.
Simulem la situació: els dos usuaris afegeixen una segona línia diferent a l’arxiu. El primer fa push sense problemes, però el segon trobarà l’error.
El primer fa:
1$ echo "segona línia 1" >> arxiu.txt
1$ git add arxiu.txt
1$ git commit -m "segona 1"
[master 8bf099d] segona 1
1 file changed, 1 insertion(+)
1$ git push
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Writing objects: 100% (3/3), 262 bytes | 262.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To https://github.com/usuari/repositori.git
17466a8..8bf099d master -> master
1$ git status
On branch master
Your branch is up to date with 'origin/master'.
nothing to commit, working tree clean
I el segon:
2$ echo "segona línia 2" >> arxiu.txt
2$ git add arxiu.txt
2$ git commit -m "segona 2"
[master eacb48e] segona 2
1 file changed, 1 insertion(+)
2$ git push
To https://github.com/usuari/repositori.git
! [rejected] master -> master (fetch first)
error: failed to push some refs to 'https://github.com/usuari/repositori.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
Com es veu, es diu que cal fer primer pull, ja que no pots fer push si no has integrat els canvis remots al teu repositori.
Provem de fer-ho. Primer el fetch:
2$ git fetch
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Total 3 (delta 0), reused 3 (delta 0), pack-reused 0
Unpacking objects: 100% (3/3), done.
From https://github.com/usuari/repositori
17466a8..8bf099d master -> origin/master
2$ git status
On branch master
Your branch and 'origin/master' have diverged,
and have 1 and 1 different commits each, respectively.
(use "git pull" to merge the remote branch into yours)
nothing to commit, working tree clean
Ens demana el pull, farem el merge (ja hem fet el fetch).
Important: fes sempre el merge amb el working directory net (sense canvis pendents de commit). Si tens canvis sense cometre, primer fes commit o guarda’ls amb
git stash.
2$ git merge
Auto-merging arxiu.txt
CONFLICT (content): Merge conflict in arxiu.txt
Automatic merge failed; fix conflicts and then commit the result.
$ git status
On branch master
Your branch and 'origin/master' have diverged,
and have 1 and 1 different commits each, respectively.
(use "git pull" to merge the remote branch into yours)
You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: arxiu.txt
no changes added to commit (use "git add" and/or "git commit -a")
Ja tenim el conflicte a arxiu.txt. Això es tradueix en el fet que git modifica l’arxiu del conflicte per a reflectir les dues versions, afegint tres delimitadors:
- <<<<<<< HEAD
- La versió local
- =======
- La versió remota
- >>>>>>> nom_de_la_branca
En el nostre cas:
2$ cat arxiu.txt
Hola, món!
<<<<<<< HEAD
segona línia 2
=======
segona línia 1
>>>>>>> refs/remotes/origin/master
En aquest punt, ens podríem fer enrere (no ho farem) fins a l’estat anterior del merge fent git merge --abort.
Ens diu que teníem “segona línia 2” (HEAD) i que al remot tenim “segona línia 1” (refs/remotes/origin/master). Per resoldre el conflicte manualment, hem d’editar aquest arxiu i decidir què fem, esborrant les línies delimitadores (<,=,>) i tot el que no ens interessi.
En el nostre cas, decidim que ni una línia ni l’altra: “segona línia 12”. Editem l’arxiu:
2$ cat arxiu.txt
Hola, món!
segona línia 12
Després d’editar-lo, cal fer git add per marcar el conflicte com a resolt i ja podem fer commit i push:
2$ git add arxiu.txt
2$ git commit -m "resolt!"
[master 6fada39] resolt!
2$ git status
On branch master
Your branch is ahead of 'origin/master' by 2 commits.
(use "git push" to publish your local commits)
nothing to commit, working tree clean
2$ git push
Enumerating objects: 10, done.
Counting objects: 100% (10/10), done.
Delta compression using up to 8 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (6/6), 523 bytes | 523.00 KiB/s, done.
Total 6 (delta 0), reused 0 (delta 0)
To https://github.com/usuari/repositori.git
8bf099d..6fada39 master -> master
2$ git status
On branch master
Your branch is up to date with 'origin/master'.
nothing to commit, working tree clean
2$ git log --graph --oneline
* 6fada39 (HEAD -> master, origin/master) resolt!
|\
| * 8bf099d segona 1
* | eacb48e segona 2
|/
* 17466a8 primer commit
Es poden veure els dos commits en paral·lel, i com finalment hi ha un commit (6fada39) que resol el problema.
Ara tornem al repositori 1:
1$ git fetch
remote: Enumerating objects: 10, done.
remote: Counting objects: 100% (10/10), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 6 (delta 0), reused 6 (delta 0), pack-reused 0
Unpacking objects: 100% (6/6), done.
From https://github.com/usuari/repositori
8bf099d..6fada39 master -> origin/master
1$ git status
On branch master
Your branch is behind 'origin/master' by 2 commits, and can be fast-forwarded.
(use "git pull" to update your local branch)
nothing to commit, working tree clean
1$ git log --graph --oneline
* 8bf099d (HEAD -> master) segona 1
* 17466a8 primer commit
1$ git merge
Updating 8bf099d..6fada39
Fast-forward
arxiu.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
1$ git status
On branch master
Your branch is up to date with 'origin/master'.
nothing to commit, working tree clean
1$ git log --graph --oneline
* 6fada39 (HEAD -> master, origin/master, origin/HEAD) resolt!
|\
| * 8bf099d segona 1
* | eacb48e segona 2
|/
* 17466a8 primer commit
I ja tenim els dos repositoris sincronitzats després del conflicte.
Estratègies de branques
El flux descrit en aquest document és trunk-based: tothom treballa directament a master sense crear branques. Això és l’enfocament més simple i adequat per a grups de 2 persones amb bona comunicació. Però existeixen altres estratègies que convé conèixer.
Trunk-based (sense branques)
Cada membre fa commits i pushes directament a master. La integració és contínua i el feedback és immediat.
# Al principi de la sessió: sincronitzar
git pull origin master
# Treballar, fer commits
git add arxius_modificats
git commit -m "descripció del canvi"
git push origin master
Feature branches (branques per tasca)
Cada unitat de treball viu en una branca de curta durada (p. ex., feature/nova-funcio). La branca es fusiona a master quan la tasca és completa i provada.
# Crear una branca per la tasca
git checkout -b feature/nova-funcio
# Treballar i fer commits
git add arxius_modificats
git commit -m "descripció del canvi"
# Quan la tasca és llesta, fusionar a master
git checkout master
git pull origin master
git merge feature/nova-funcio
git push origin master
# Neteja
git branch -d feature/nova-funcio
Comparació
| Trunk-based | Feature branches | |
|---|---|---|
| Complexitat git | Baixa | Mitjana |
| Risc de conflictes | Baix (si sincronitzen sovint) | Mig (si branques viuen > 3 dies) |
| Aïllament del treball | Cap: un error afecta tothom | Alt: errors queden a la branca |
| Recomanat per a 2 persones | Si | Opcional |
| Recomanat per a 3+ persones | Possible | Si |
Les branques de curta durada (2-3 dies) funcionen bé. Les de llarga durada (> 1 setmana) divergeixen molt de master i els merges es compliquen. La regla pràctica: una tasca, una branca, fusiona ràpid.
Flux d’aprovació (merge controlat)
Quan es treballa amb feature branches, el merge a la branca principal es pot fer de dues maneres: directament pel desenvolupador, o bé a través d’un flux d’aprovació on algú revisa els canvis abans d’integrar-los. Aquesta segona opció és la base de les pull requests o merge requests que ofereixen moltes plataformes, però la idea de fons és purament organitzativa i es pot implementar amb qualsevol servidor git.
La idea
El principi és senzill: separar qui proposa canvis de qui els accepta. Un desenvolupador treballa a la seva branca i, quan la tasca és llesta, demana a un responsable que revisi i integri els canvis a la branca principal. El responsable pot:
- Revisar el diff dels canvis proposats.
- Demanar correccions si cal.
- Acceptar i fer el merge quan tot és correcte.
Això afegeix una capa de revisió que millora la qualitat del codi i evita que canvis no revisats arribin a la branca principal.
El flux amb git pur
Aquest flux no requereix cap eina especial, només git i una convenció d’equip:
1. El desenvolupador treballa a la seva branca i la puja al repositori remot:
git checkout -b feature/nova-funcio
# ... treballa, fa commits ...
git push -u origin feature/nova-funcio
El -u associa la branca local amb la remota (estableix l’upstream), de manera que les properes vegades n’hi ha prou amb git push.
2. Avisa al responsable (per correu, xat, o qualsevol canal) que la branca està llesta per revisar.
3. El responsable revisa els canvis des del seu repositori local:
git fetch origin
git log --oneline master..origin/feature/nova-funcio # quins commits hi ha
git diff master..origin/feature/nova-funcio # què canvien
4. Si cal demanar correccions, ho comunica al desenvolupador. El desenvolupador fa els canvis a la mateixa branca, commit i git push (ja no cal especificar el remot ni la branca gràcies al -u inicial). El responsable torna a revisar.
5. Quan els canvis són acceptats, el responsable fa el merge:
git checkout master
git pull origin master
git merge origin/feature/nova-funcio
git push origin master
# Neteja de la branca remota
git push origin --delete feature/nova-funcio
6. El desenvolupador sincronitza i neteja en local:
git checkout master
git pull origin master
git branch -d feature/nova-funcio
Permisos sobre branques
Per reforçar aquest flux, es poden configurar permisos d’escriptura sobre les branques del repositori remot. Per exemple, restringir l’escriptura a master a un sol usuari o a un grup reduït. D’aquesta manera, els desenvolupadors poden pujar les seves branques de feature però no poden fer push directament a master: només el responsable pot fer-ho després de revisar.
Això converteix una convenció organitzativa en una restricció tècnica. La majoria de servidors git (inclosos els auto-allotjats) permeten configurar aquests permisos.
Quan val la pena
Per a equips petits (2-3 persones) amb bona comunicació, un flux d’aprovació formal pot ser excessiu. Però a mesura que l’equip creix o el projecte requereix més rigor, afegir una capa de revisió abans del merge és una de les pràctiques que més millora la qualitat del codi sense complicar excessivament el flux de treball.
Bones pràctiques
Missatges de commit
Un bon missatge de commit permet al teu company entendre el canvi sense llegir el diff. Dues convencions habituals:
Imperatiu simple (mínim de fricció):
git commit -m "add user validation"
git commit -m "fix null check in preprocessing"
git commit -m "remove unused imports"
Regla: comença amb un verb en imperatiu. Descriu què fa el commit, no el que has fet tu. Menys de 72 caràcters.
Conventional Commits (més estructurat):
git commit -m "feat: add predict endpoint"
git commit -m "fix: handle null age field"
git commit -m "test: add unit tests for validator"
git commit -m "docs: update README with examples"
git commit -m "chore: update dependencies"
Prefixes: feat (nova funcionalitat), fix (correcció), test (tests), docs (documentació), chore (manteniment), refactor (refactorització sense canvi de comportament).
| Imperatiu simple | Conventional Commits | |
|---|---|---|
| Fricció d’escriptura | Baixa | Mitjana (cal recordar el prefix) |
| Llegibilitat del log | Bona | Molt bona |
| Compatible amb eines de changelog | No | Si |
Commits atòmics
Un commit atòmic conté un sol canvi lògic. El codi ha de funcionar després de cada commit. Això contrasta amb fer un sol commit gran al final del dia amb tots els canvis barrejats.
Per què importa? Quan alguna cosa falla, els commits atòmics fan evident quin canvi ha causat el problema:
# Commit gran: on és el problema?
$ git log --oneline -1
a3f9b12 "add model, preprocessing, API and tests"
# Commits atòmics: el problema és al segon commit
$ git log --oneline -4
a3f9b12 "add predict endpoint"
def5678 "add logistic regression model" ← falla aquí
c72a3f8 "add preprocessing pipeline"
5e8d1a0 "add FastAPI skeleton"
Regla pràctica: fes commit de cada unitat lògica de treball per separat. Si el codi no està llest, utilitza git stash per guardar-lo temporalment sense fer commit.
Git i CI/CD
En molts projectes, el repositori git no és només un lloc on guardar codi: és el punt de connexió amb sistemes d’integració contínua (CI) i desplegament continu (CD). Entendre aquesta relació ajuda a treballar amb més cura.
Com funciona la connexió
Les plataformes com github i gitlab permeten configurar pipelines que reaccionen automàticament a events git:
- Push a una branca (p. ex.,
master): pot disparar l’execució de tests, anàlisi de codi, o compilació. Si els tests fallen, l’equip rep una notificació. - Push d’un tag: pot disparar un desplegament a producció o a un entorn de proves.
- Creació d’una pull/merge request: pot executar els tests sobre la branca abans que el merge sigui acceptat.
Això significa que cada git push té conseqüències més enllà del repositori: pot posar en marxa processos automàtics que afecten tot l’equip o fins i tot els usuaris finals.
Implicacions pràctiques
- Un push descuidat pot bloquejar el CI per a tot l’equip. Si fas push de codi que no compila o que trenca els tests, el pipeline fallarà i ningú podrà validar els seus canvis fins que es corregeixi.
- Els tags són decisions de desplegament. Un tag no és només una etiqueta: en molts projectes, crear un tag actualitza el producte en producció. Cal crear-los de forma deliberada.
- Testar en local abans de fer push. Executar els tests localment abans de pujar canvis evita cicles innecessaris de CI i estalvia temps a tot l’equip.
- L’ordre importa. Si el sistema CI reacciona a tags, cal assegurar-se que el commit ja és a la branca principal abans de crear el tag. L’ordre habitual és: primer
git pushdel commit, desprésgit pushdel tag.
git push origin master ← CI valida el codi
git tag -a v1.0 -m "versió 1"
git push origin v1.0 ← CI desplega a producció
Configuració del CI/CD
La majoria de plataformes git (github, gitlab, gitea, etc.) inclouen eines de CI/CD integrades. La configuració es fa típicament mitjançant arxius YAML al propi repositori, on es defineixen quins passos s’executen (tests, compilació, desplegament) i en resposta a quins events git (push, tag, merge request).
Regles d’or del treball en equip
git pull(ofetch+merge) primer, sempre – Sincronitza abans de treballar per minimitzar conflictes.- Commits atòmics amb missatges llegibles – Un commit, una idea; el teu company ha d’entendre el canvi sense llegir el diff.
- No fer push de codi trencat – Si saps que el codi no funciona, no el pugis. Trenca el flux de treball de tot l’equip.
- Comunicar-se amb l’equip – Avisa abans de fer canvis grans o resets. Un missatge ràpid evita molts conflictes.
FAQ
Com crear un repositori a partir d’una carpeta existent?
Si ja tens una carpeta amb codi i vols pujar-la a un repositori remot buit (creat prèviament a github o gitlab):
cd la-meva-carpeta
git init
git symbolic-ref HEAD refs/heads/main
git remote add origin https://gitlab.com/usuari/repositori.git
# afegir .gitignore abans del primer commit
git add .
git commit -m "commit inicial"
git push -u origin main
Alternativament, pots clonar primer el repositori buit i copiar-hi el contingut:
git clone https://gitlab.com/usuari/repositori.git
cd repositori
git switch -c main
# copiar arxius i afegir .gitignore
git add .
git commit -m "commit inicial"
git push -u origin main
Quina diferència hi ha entre git pull i git fetch + git merge?
git pull és equivalent a fer git fetch seguit de git merge. La diferència pràctica és que amb fetch + merge pots inspeccionar els canvis remots abans d’integrar-los (amb git log o git diff), mentre que pull ho fa tot de cop.
# Amb fetch + merge (més control)
git fetch
git log --oneline master..origin/master # veure què ha canviat
git merge
# Amb pull (més ràpid)
git pull origin master
Com s’associa una branca local amb una branca remota?
Quan fas git push o git pull sense especificar el remot ni la branca, git necessita saber a quina branca remota correspon la teva branca local. Aquesta associació s’anomena upstream o tracking branch.
Hi ha diverses maneres d’establir-la:
# Opció 1: al fer push per primera vegada, amb -u (o --set-upstream-to)
git push -u origin main
# Opció 2: explícitament, sense fer push
git branch --set-upstream-to=origin/main main
Un cop establerta l’associació, pots fer servir les comandes curtes:
git push # equivalent a git push origin main
git pull # equivalent a git pull origin main
Quan clones un repositori, git configura automàticament el tracking de la branca principal. Per això git pull funciona directament en un repositori clonat sense haver de fer -u primer.
Per veure quines branques locals tenen upstream configurat:
git branch -vv
La sortida mostra, entre claudàtors, la branca remota associada:
* main a1b2c3d [origin/main] últim missatge de commit
feature d4e5f6a [origin/feature] un altre missatge
He fet commit d’un arxiu que no hauria d’haver afegit. Com ho desfaig?
Si encara no has fet push, pots treure l’arxiu de l’últim commit sense perdre els canvis locals:
git reset HEAD~1 --soft # desfà el commit, manté els canvis al staging
git reset HEAD arxiu-erroni.txt # treu l'arxiu del staging
git commit -m "el commit correcte"
Si ja has fet push però vols treure l’arxiu del repositori sense esborrar-lo del disc:
git rm --cached arxiu-erroni.txt
# afegir-lo al .gitignore si cal
git commit -m "remove arxiu-erroni del repositori"
git push
Com puc veure els canvis que he fet abans de fer commit?
git diff # canvis al working directory (no afegits al staging)
git diff --staged # canvis ja afegits al staging area
git diff HEAD # tots els canvis respecte l'últim commit
He modificat arxius però vull descartar tots els canvis i tornar a l’últim commit
git reset --hard HEAD
Compte: això esborra tots els canvis no comesos. Si vols guardar-los temporalment per recuperar-los més tard:
git stash # guarda els canvis temporalment
# ... fas altres coses ...
git stash pop # recupera els canvis guardats
Com puc desfer o corregir l’últim commit (si no he fet push)?
Mentre no hagis fet push, l’últim commit es pot desfer o modificar sense afectar ningú.
Desfer el commit completament, mantenint els canvis al working directory:
git reset --soft HEAD~1 # els canvis queden al staging, llestos per tornar a fer commit
git reset HEAD~1 # els canvis queden al working directory, fora del staging
Desfer el commit i descartar els canvis (irreversible):
git reset --hard HEAD~1
Corregir l’últim commit (afegir arxius oblidats o canviar el missatge):
# Canviar només el missatge
git commit --amend -m "nou missatge corregit"
# Afegir arxius que faltaven al commit
git add arxiu-oblidat.txt
git commit --amend --no-edit # manté el missatge original
--amend reescriu l’últim commit. Si ja has fet push, no és recomanable fer-ho perquè reescriu l’historial i pot causar problemes als companys.
Com puc tornar el repositori a l’estat que hi ha remotament?
Si vols descartar tots els canvis locals (commits, staging i working directory) i sincronitzar-te exactament amb el repositori remot:
git fetch origin
git reset --hard origin/master
Això mou el HEAD local al mateix commit que origin/master, descartant qualsevol commit local que no s’hagi pujat i qualsevol canvi pendent. Si tens arxius nous que no estan al repositori (untracked), no s’esborren. Per eliminar-los també:
git clean -fd # esborra arxius i carpetes untracked
Compte: ambdues operacions són irreversibles. Si no estàs segur de voler perdre els canvis, fes primer una còpia o un git stash.
Referències
- git/github guide
- Pro Git book (gratuït)
- Conventional Commits
- Semantic Versioning
- Atlassian: Comparing workflows
Docker bàsic
- Introducció
- Conceptes fonamentals
- Primers passos
- Dockerfile: crear imatges pròpies
- Volums i variables d’entorn
- Docker Compose
- Distribució d’imatges
- Resum de comandes
- Annexos
- Recursos addicionals
Introducció
Les màquines virtuals emulen una màquina; els contenidors despleguen una aplicació. Docker és una plataforma que empaqueta una aplicació amb tot el que necessita — i només el que necessita — perquè el kernel del host ja s’encarrega de la resta. D’aquest principi en deriven tres propietats que defineixen com es treballa amb Docker:
- Predictibilitat: el comportament és idèntic al portàtil i a producció — s’acaba el “a la meva màquina funciona”.
- Aïllament: cada contenidor té les seves pròpies dependències, sense conflictes entre projectes.
- Eficiència: sense OS convidat ni serveis de sistema innecessaris, els contenidors arrenquen en mil·lisegons i ocupen MBs en lloc de GBs.
Aquesta filosofia va impulsar els microserveis: sistemes formats per molts contenidors petits, cadascun amb una sola responsabilitat, escalables i substituïbles de manera independent.
Contenidors vs Màquines Virtuals

Una màquina virtual inclou un Guest OS complet (kernel + espai d’usuari) sobre un hipervisor. Cada VM emula maquinari i arrenca el seu propi sistema operatiu, cosa que implica GBs de disc i minuts d’arrencada.
Un contenidor comparteix el kernel del host i utilitza funcionalitats del kernel Linux — namespaces, cgroups — per aïllar processos sense necessitar un OS complet (veure annex tècnic per a més detalls).
| Màquina Virtual | Contenidor | |
|---|---|---|
| Aïllament | Complet (Guest OS propi) | A nivell de procés (namespaces) |
| Pes | GBs | MBs |
| Arrencada | Minuts | Segons |
| Rendiment | Overhead de virtualització | Quasi natiu |
| Kernel | Propi (Guest OS) | Compartit amb el host |
Conceptes fonamentals
Imatge (Image)
Una imatge és una plantilla de només lectura que conté tot el necessari per executar una aplicació: sistema operatiu mínim, runtime, dependencies i codi. Les imatges es construeixen a partir d’un Dockerfile. Internament, una imatge és una pila de capes de només lectura. Això permet reutilitzar capes entre imatges: si dues imatges parteixen de la mateixa base (per exemple ubuntu:22.04), aquella capa es guarda una sola vegada al disc i es comparteix. Quan descarregues una imatge que té capes que ja tens d’una altra, Docker només baixa les que falten.
Contenidor (Container)
Un contenidor és una instància en execució d’una imatge. Podem tenir múltiples contenidors de la mateixa imatge, cadascun aïllat dels altres. Quan en crees un, Docker afegeix una capa d’escriptura a sobre de les capes de la imatge — és on van tots els canvis que fa l’aplicació en temps d’execució. Les capes de la imatge original mai es modifiquen, per això múltiples contenidors poden compartir la mateixa imatge simultàniament.
Analogia:
- Imatge = Recepta de cuina (plantilla)
- Contenidor = Pastís cuinat (instància)
Dockerfile
Un Dockerfile és un fitxer de text amb instruccions per construir una imatge. Per a Docker, representa:
- Instruccions pas a pas — Docker executa cada línia en ordre i genera una capa per cada pas. Si la construcció falla, saps exactament on i per què.
- L’entorn escrit, no recordat — en lloc de documentar “instal·la això, configura allò”, el Dockerfile és el document. Qualsevol pot llegir-lo i saber exactament què conté la imatge.
- Canvis controlats — per actualitzar una dependency, edites el Dockerfile i reconstrueixes. No es modifica un contenidor en execució a mà.
- Mateix resultat a qualsevol lloc — el mateix Dockerfile produeix la mateixa imatge al portàtil i a producció.
Registre (Registry)
Un registre és un repositori d’imatges. El més popular és Docker Hub, però en entorns professionals l’ús de registres privats és la norma, no l’excepció.
Quan fas docker run nginx, Docker primer busca la imatge nginx localment. Si no la troba, la descarrega automàticament de Docker Hub. Això és un pull implícit — la majoria de vegades ni te n’adones.
Primers passos
Hands-on — Segueix els passos a la teva terminal.
Un cop instal·lat Docker (veure annex), comprovem que funciona:
docker run hello-world
Aquesta comanda descarrega la imatge hello-world de Docker Hub, crea un contenidor i l’executa. Si veus el missatge de benvinguda, Docker funciona correctament.
Executar un contenidor: el cicle bàsic
Un contenidor passa per tres estats al llarg de la seva vida:
- En execució — el PID 1 està viu i el contenidor fa la seva feina.
docker run - Aturat — el PID 1 ha mort (per error, per
docker stop, o perquè ha acabat). El contenidor encara existeix amb el seu sistema de fitxers. - Eliminat — el contenidor desapareix completament.
docker rm
La distinció important és entre aturat i eliminat: un contenidor aturat encara ocupa espai en disc (la capa d’escriptura es conserva) i es pot tornar a arrencar. docker ps només mostra els contenidors en execució — docker ps -a mostra tots, inclosos els aturats.
Quan executes docker stop, Docker envia SIGTERM al PID 1 perquè pugui tancar-se de manera ordenada. Si no ha acabat en 10 segons, envia SIGKILL. Tots els processos fills moren amb ell i els namespaces i cgroups es desmunten. Si vols que el contenidor s’elimini automàticament en aturar-se, usa --rm:
docker run --rm nginx # el contenidor desapareix quan s'atura
Anem a treballar amb nginx (un servidor web) per veure el cicle de vida d’un contenidor. Primer, l’executem en segon pla (-d):
docker run -d -p 8080:80 --name web nginx
El flag -p 8080:80 és el mapatge de ports (port mapping). Un contenidor té la seva pròpia xarxa aïllada — nginx escolta al port 80 dins del contenidor, però sense -p aquest port és invisible des del host. El mapatge host:contenidor obre un pont: les peticions que arriben al port 8080 del host es reenvien al port 80 del contenidor.
navegador → localhost:8080 (host) → :80 (contenidor nginx)
El port del host i el del contenidor no cal que siguin iguals. Aquí usem 8080 al host perquè el 80 pot estar ocupat o requerir permisos de root.
Ara podem comprovar que està funcionant amb docker ps, que mostra els contenidors actius:
docker ps
# CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# a518f8cca58b nginx "/docker-entrypoint.…" 5 minutes ago Up 5 minutes 0.0.0.0:8080->80/tcp, [::]:8080->80/tcp web
La sortida ens diu que el contenidor web està executant la imatge nginx, porta 5 minuts actiu (Up 5 minutes), i el mapatge de ports confirma que el port 8080 del host (tant IPv4 com IPv6) redirigeix al port 80 del contenidor.
Accedim a http://localhost:8080 al navegador — veurem la pàgina per defecte de nginx.
Podem veure quines imatges tenim descarregades i l’espai que ocupen amb docker images:
docker images
# IMAGE ID DISK USAGE CONTENT SIZE EXTRA
# hello-world:latest ef54e839ef54 25.9kB 9.52kB U
# nginx:latest 0236ee02dcbc 240MB 65.8MB U
Per veure què està fent el contenidor, consultem els seus logs:
docker logs web
docker logs -f web # -f: seguir en temps real (Ctrl+C per sortir)
Si necessitem entrar dins del contenidor per inspeccionar-lo o debugar:
docker exec -it web bash
# Ara estem dins del contenidor amb una shell interactiva
# -i: interactiu (mantenir STDIN obert)
# -t: terminal (assignar una pseudo-TTY)
Per aturar i eliminar el contenidor:
docker stop web
docker rm web
docker stop envia un senyal SIGTERM al PID 1 donant-li temps per acabar de forma neta, mentre que docker kill envia SIGKILL que atura el procés immediatament sense possibilitat de cleanup — per això és important que el PID 1 sigui el procés de l’aplicació i no un shell. docker rm elimina el contenidor aturat — sense aquest pas, el contenidor queda en estat aturat ocupant espai en disc (visible amb docker ps -a).
A la pràctica, els contenidors aturats rarament són útils — com a molt serveixen per inspeccionar el sistema de fitxers d’un contenidor que ha fallat. En producció, els contenidors es tracten com a descartables: si fallen, se’n crea un de nou en lloc de reiniciar l’antic. Per això, la majoria de contenidors es llancen amb --rm, que els elimina automàticament en aturar-se:
docker run --rm -d -p 8080:80 nginx
Flags més comuns de docker run
| Flag | Significat | Exemple |
|---|---|---|
-d | Execució en segon pla (detached) | docker run -d nginx |
-it | Shell interactiva | docker run -it ubuntu bash |
-p | Mapejar ports (host:contenidor) | -p 8080:80 |
--name | Assignar un nom al contenidor | --name web |
--rm | Eliminar automàticament en aturar | docker run --rm nginx |
-e | Variable d’entorn | -e LOG_LEVEL=debug |
-v | Muntar volum (veure Volums) | -v ./data:/app/data |
Dockerfile: crear imatges pròpies
Un Dockerfile defineix pas a pas com construir la nostra imatge. Cada instrucció crea una capa (layer) — un diff sobre l’estat anterior del sistema de fitxers. La imatge final és l’apilament de totes aquestes capes:
[ layer 4 ] COPY app/ ./ ← el teu codi
[ layer 3 ] RUN pip install ... ← les dependències
[ layer 2 ] COPY requirements.txt ← el fitxer de dependències
[ layer 1 ] FROM python:3.12-slim ← el sistema base
Docker guarda cada capa en cache. Quan reconstrueixes la imatge, només recalcula les capes que han canviat i totes les que en depenen. Per això l’ordre importa: si poses COPY app/ abans de RUN pip install, qualsevol canvi al codi invalida la capa de dependències i Docker les torna a instal·lar des de zero. Posant COPY requirements.txt i RUN pip install abans de copiar el codi, les dependències es mantenen en cache mentre requirements.txt no canviï.
Estructura bàsica
# Dockerfile
# FROM: imatge base (obligatòria, sempre primera)
FROM python:3.12-slim
# WORKDIR: directori de treball dins del contenidor
WORKDIR /app
# COPY: copiar fitxers del host al contenidor
# Primer les dependencies (canvien poc → cache eficient)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Després el codi (canvia sovint)
COPY . .
# EXPOSE: documentar quin port usa l'app (no el publica!)
EXPOSE 8000
# CMD: comando per defecte quan s'executa el contenidor
CMD ["python", "app.py"]
Etiquetes d’imatges base
Les imatges oficials ofereixen variants amb diferents mides:
python:3.12— Imatge completa (gran, ~900MB)python:3.12-slim— Versió lleugera, sense eines de compilació (~150MB)python:3.12-alpine— Mínima, basada en Alpine Linux (~50MB, pot donar problemes de compatibilitat)
Per a producció, usa sempre una versió específica (python:3.12-slim), mai latest.
CMD i ENTRYPOINT
Quan un contenidor arrenca, el kernel necessita llançar exactament un procés per iniciar-ho tot. Aquest procés rep el número 1 — el PID 1 — i té un rol especial: quan ell mor, el contenidor s’atura. Tot el que passi dins del contenidor és fill seu.
En una aplicació d’inferència, volem que aquest procés sigui uvicorn directament.
ENTRYPOINT i CMD es reparteixen aquesta responsabilitat de forma complementària:
ENTRYPOINTdefineix el que és el contenidor. Un contenidor que serveix prediccions sempre arrencaràuvicorn, independentment de com l’executis.CMDdefineix com s’executa per defecte. Quina app, quin port, quins flags. Es pot sobreescriure endocker runsense reconstruir la imatge.
Aquesta separació té una conseqüència pràctica molt útil: pots tenir una imatge única i arrencar-la de formes diferents.
# Arrencada normal
docker run model-api
# Arrencada amb port diferent, sense reconstruir la imatge
docker run model-api app.main:app --host 0.0.0.0 --port 9000
# Inspecció interactiva, substituint completament el CMD
docker run --entrypoint /bin/bash -it model-api
La bona pràctica és sempre usar la forma exec (llista JSON) tant per ENTRYPOINT com per CMD:
ENTRYPOINT ["uvicorn"]
CMD ["app.main:app", "--host", "0.0.0.0", "--port", "8000"]
El nom “forma exec” ve del comportament de la comanda exec en un shell: en lloc de crear un procés fill, substitueix el procés actual pel nou — heretant el mateix PID. Això és exactament el que fa Docker amb la forma exec: uvicorn es converteix en el PID 1 directament, rep els senyals del sistema operatiu, gestiona els seus workers correctament, i quan fas docker stop el contenidor s’atura de forma neta i controlada.
Construir i executar
Hands-on — Segueix els passos a la teva terminal.
Posem en pràctica el Dockerfile de l’Estructura bàsica. Creem un directori amb tres fitxers:
myapp/
├── Dockerfile
├── requirements.txt
└── app.py
# app.py
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "Hello from Docker!"
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)
# requirements.txt
flask
El Dockerfile és el de la secció anterior (amb el CMD ajustat al port 8000). Per construir la imatge, usem docker build. El flag -t assigna un nom i etiqueta en format nom:versió. El . al final és el context del build — el directori que Docker envia al dimoni per construir la imatge (normalment l’arrel del projecte, on hi ha el Dockerfile i el codi):
docker build -t myapp:1.0 .
docker run --rm -d -p 8000:8000 --name myapp myapp:1.0
Accedim a http://localhost:8000 — veurem “Hello from Docker!”.
Per forçar que es reconstrueixin totes les capes ignorant la cache (útil quan les dependencies externes han canviat i no es reflecteix als fitxers locals):
docker build --no-cache -t myapp:1.0 .
ARG: variables de temps de build
ARG defineix variables disponibles només durant el docker build — no persisteixen al contenidor en execució. S’utilitzen per parametritzar el build sense hardcodejar valors al Dockerfile:
ARG PYTHON_VERSION=3.12
FROM python:${PYTHON_VERSION}-slim
En construir, podem sobreescriure el valor per defecte amb --build-arg:
docker build --build-arg PYTHON_VERSION=3.12 .
La distinció important: si necessites que l’aplicació pugui llegir un valor en execució, usa ENV. Si només el necessites durant el build (versió de la imatge base, flags de compilació), usa ARG. Un error comú és usar ARG per configurar paths o credencials i després no entendre per què l’app no els troba quan arrenca.
.dockerignore
El fitxer .dockerignore funciona com .gitignore — evita copiar fitxers innecessaris a la imatge, fent-la més petita i segura:
.git/
.venv/
__pycache__/
*.md
tests/
.env
node_modules/
Directoris com .venv/ i node_modules/ s’exclouen perquè les dependències del host poden estar compilades per una arquitectura o sistema operatiu diferent del contenidor. El Dockerfile ja les instal·la des de zero amb RUN pip install o RUN npm install, assegurant que són compatibles amb l’entorn del contenidor.
Bones pràctiques
Imatges lleugeres — Usa variants -slim o -alpine de les imatges base. La diferència de mida pot ser enorme (900MB vs 50MB). Si instal·les paquets del sistema, neteja la cache en la mateixa instrucció RUN:
RUN apt-get update && \
apt-get install -y --no-install-recommends curl && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
Ordre de les capes — Si una capa canvia, totes les capes posteriors es reconstrueixen. Per això, copia primer els fitxers de dependencies (que canvien poc) i després el codi:
# BÉ: dependencies primer
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
# MALAMENT: qualsevol canvi al codi invalida la cache de pip install
COPY . .
RUN pip install -r requirements.txt
No executar com root — Els processos dins d’un contenidor s’executen com a root per defecte. Crea un usuari sense privilegis i canvia a ell abans d’arrencar l’aplicació:
RUN useradd -m -u 1000 appuser && \
chown -R appuser:appuser /app
USER appuser
Usar -u 1000 explícitament fa que l’UID de l’usuari dins del contenidor coincideixi amb el del primer usuari no-root de la majoria de distribucions Linux. Això és important quan muntes bind mounts: el host identifica propietaris de fitxers per UID, no per nom d’usuari. Si el contenidor escriu fitxers amb UID 1000 i el teu usuari del host també és 1000, els fitxers et pertanyeran i podràs llegir-los i modificar-los sense problemes de permisos.
Multi-stage builds
Sense multi-stage, una imatge de producció acaba contenint tot el que vas necessitar durant el build: la imatge base completa, eines de compilació, paquets de test i codi font innecessari. La imatge resultant pot pesar 1GB o més quan l’únic que necessites per executar l’API és Python, les dependencies de producció i el codi de l’aplicació.
Els multi-stage builds solucionen això separant el procés en stages independents. La imatge resultant és només l’últim stage — els anteriors són temporals i es descarten després del build. Només el que copies explícitament amb COPY --from= arriba a la imatge final:
# Dockerfile
# Stage 1: Builder — imatge completa amb eines de compilació (gcc, headers...)
# Necessària quan les dependències tenen extensions en C (numpy, psycopg2, cryptography...)
FROM python:3.12 AS builder
WORKDIR /app
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Stage 2: Runtime — imatge lleugera, només el necessari per executar
# gcc i les eines de compilació no arriben aquí
FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY app/ ./app/
ENTRYPOINT ["uvicorn"]
CMD ["app.main:app", "--host", "0.0.0.0", "--port", "8000"]
AS builder dóna nom al primer stage. El builder usa la imatge completa (python:3.12) que inclou gcc i eines de compilació necessàries per instal·lar dependències amb extensions en C. Instal·la tot dins d’un virtualenv a /opt/venv, que després es copia sencer al stage de runtime — així tant els paquets com els executables (com uvicorn) arriben a la imatge final. El runtime usa python:3.12-slim, molt més lleugera. El codi font, notebooks, fitxers .env i dades de test no arriben mai a la imatge final. Els beneficis: imatge fins a 10x més petita, menys superfície d’atac, i separació clara entre el que cal per compilar i el que cal per executar.
Volums i variables d’entorn
Volums: persistir dades
Per defecte, les dades dins d’un contenidor es perden quan s’elimina. Els volums resolen aquest problema. Hi ha dos tipus principals:
Named volumes — gestionats per Docker, ideals per a dades persistents (bases de dades, logs):
docker run -d -v app_logs:/app/logs myapp:1.0
# app_logs: nom del volum (Docker el gestiona)
# /app/logs: path dins del contenidor
Bind mounts — munten un directori del host, ideals per a desenvolupament (els canvis es reflecteixen immediatament):
docker run -d -v $(pwd)/src:/app/src myapp:1.0
# $(pwd)/src: directori del host
# /app/src: directori del contenidor
| Tipus | Quan usar | Gestió |
|---|---|---|
| Named volume | Producció, dades persistents | Docker |
| Bind mount | Desenvolupament, compartir fitxers | Host |
En la pràctica, els named volumes es declaren a compose.yml en lloc de passar-los per línia de comandes — veure l’exemple amb Docker Compose.
Variables d’entorn
Les variables d’entorn permeten configurar l’aplicació sense modificar el codi ni la imatge. Hi ha dues formes de definir-les:
Al Dockerfile — amb ENV, que estableix valors per defecte dins de la imatge:
ENV LOG_LEVEL=info
ENV PORT=8000
Aquests valors existiran sempre que s’executi el contenidor. Es poden sobreescriure en docker run amb -e.
En temps d’execució — amb -e o des d’un fitxer .env:
docker run -d -e LOG_LEVEL=debug -e MAX_WORKERS=4 myapp:1.0
# O des d'un fitxer .env
docker run -d --env-file .env myapp:1.0
Bones pràctiques: mai incloguis secrets (passwords, API keys) al Dockerfile ni al codi. Usa variables d’entorn o fitxers .env (que no es comitegen a git).
Hands-on: volums i variables d’entorn
Hands-on — Segueix els passos a la teva terminal. No cal cap imatge pròpia — farem servir
python:3.12-slimdirectament.
Creem un script que llegeixi configuració des de variables d’entorn i escrigui un log:
# config.py
import os
from datetime import datetime
env = os.getenv("APP_ENV", "development")
port = os.getenv("APP_PORT", "8000")
print(f"Entorn: {env} | Port: {port}")
with open("/logs/app.log", "a") as f:
f.write(f"[{datetime.now()}] Arrencada en {env}:{port}\n")
print("Log escrit a /logs/app.log")
Executem el script dins d’un contenidor combinant els tres conceptes — bind mount per al codi, variables d’entorn per a la configuració, i un named volume per als logs:
docker run --rm \
-v $(pwd):/app \
-v app_logs:/logs \
-e APP_ENV=staging \
-e APP_PORT=9000 \
python:3.12-slim python /app/config.py
# Entorn: staging | Port: 9000
# Log escrit a /logs/app.log
Executem-ho unes quantes vegades i comprovem que els logs persisteixen entre execucions — el contenidor es destrueix (--rm), però el named volume app_logs sobreviu:
docker run --rm -v app_logs:/logs python:3.12-slim cat /logs/app.log
# [2026-03-10 10:00:01] Arrencada en staging:9000
# [2026-03-10 10:00:05] Arrencada en staging:9000
Ara modifiquem config.py al host — per exemple, canviem el format del log — i tornem a executar. El canvi es reflecteix immediatament perquè el bind mount comparteix el fitxer entre host i contenidor, sense reconstruir cap imatge.
Per no repetir les variables a cada docker run, podem usar un fitxer .env:
# .env
APP_ENV=production
APP_PORT=8080
docker run --rm -v $(pwd):/app -v app_logs:/logs --env-file .env python:3.12-slim python /app/config.py
# Entorn: production | Port: 8080
Quan ja no necessitem les dades, podem eliminar el volum:
docker volume rm app_logs
Aquest patró — bind mount per al codi, variables d’entorn per a la configuració, i named volume per a dades persistents — és el flux habitual de desenvolupament local amb Docker.
Comandes principals de Docker
| Comanda | Descripció |
|---|---|
docker build -t nom:tag . | Construir una imatge des d’un Dockerfile |
docker run | Crear i executar un contenidor |
docker ps / docker ps -a | Llistar contenidors actius / tots |
docker stop <id> | Aturar un contenidor (SIGTERM → SIGKILL) |
docker rm <id> | Eliminar un contenidor aturat |
docker exec -it <id> bash | Executar comanda dins d’un contenidor en marxa |
docker logs -f <id> | Veure logs d’un contenidor |
docker images | Llistar imatges locals |
docker pull / docker push | Descarregar / pujar imatge al registre |
docker volume ls | Llistar volums |
docker network ls | Llistar xarxes |
docker system prune | Eliminar recursos no usats (imatges, contenidors, xarxes) |
Docker Compose
Quan una aplicació té múltiples serveis (API + base de dades, per exemple), gestionar cada contenidor individualment es fa feixuc. Docker Compose permet definir tots els serveis en un fitxer compose.yml i gestionar-los amb una sola comanda.
Abans i després
Hands-on — Segueix els passos a la teva terminal. Necessites la imatge
myapp:1.0construïda a Construir i executar.
Sense Compose — comandes llargues per cada servei, incloent la creació manual d’una xarxa perquè els contenidors es puguin trobar pel nom:
docker network create myapp
docker run -d --name db --network myapp -e POSTGRES_PASSWORD=secret postgres:17-alpine
docker run -d --name app --network myapp -p 8000:8000 -e DATABASE_URL=postgresql://postgres:secret@db:5432/postgres myapp:1.0
Amb Compose — tot definit en un fitxer. Creem un compose.yml al mateix directori. Cada entrada dins de services defineix un contenidor:
# compose.yml
name: myapp
services:
app:
image: myapp:1.0
ports:
- "8000:8000"
environment:
- LOG_LEVEL=info
db:
image: postgres:17-alpine
environment:
- POSTGRES_PASSWORD=secret
docker compose up -d # Aixeca tots els serveis
docker compose ps # Veure l'estat dels serveis
docker compose logs app # Logs d'un servei concret
docker compose down # Atura i elimina tot
Accedim a http://localhost:8000 — veurem “Hello from Docker!” servit per l’app, mentre PostgreSQL corre en paral·lel. Amb una sola comanda hem aixecat dos serveis.
Compose també crea automàticament una xarxa compartida perquè els contenidors es trobin pel nom — sense necessitat de docker network create.
Nota: aquest exemple no declara volums, així que les dades de PostgreSQL es perden amb
docker compose down. Veure Volums per a persistència.
Comandes principals de Compose
Compose tradueix operacions complexes de Docker en comandes simples:
| Compose | Equivalent Docker CLI | Descripció |
|---|---|---|
docker compose build | docker build | Construir les imatges dels serveis |
docker compose up | docker build + docker network create + docker run | Crear/arrencar els serveis |
docker compose down | docker stop + docker rm + docker network rm | Aturar i eliminar tot |
docker compose run | docker run | Contenidor puntual |
docker compose exec | docker exec | Comanda dins d’un contenidor en marxa |
docker compose logs | docker logs (per cada contenidor) | Logs de tots els serveis |
docker compose ps | docker ps (filtrat) | Estat dels serveis |
up — arrenca tots els serveis (o els que especifiquis) tal com estan definits al compose.yml. És el flux normal de treball: aixecar l’aplicació sencera. Si la imatge no existeix i hi ha build:, la construeix. Amb -d s’executa en segon pla:
docker compose up -d # tots els serveis
docker compose up -d api db # només els serveis indicats
docker compose up --build -d # reconstruir imatges abans d'arrencar
run — crea un contenidor nou per executar una tasca puntual i surt. Ideal per a migracions, tests, scripts o qualsevol operació que necessiti el mateix entorn (xarxa, variables, volums) però que no és un servei permanent:
docker compose run --rm api python manage.py migrate # migració de base de dades
docker compose run --rm api pytest # executar tests
docker compose run --rm api python seed.py # script puntual
El flag --rm elimina el contenidor en acabar — sense ell, s’acumulen contenidors aturats. A diferència de up, run no publica els ports per defecte (per evitar conflictes amb el servei principal); si els necessites, afegeix --service-ports.
exec — executa una comanda dins d’un contenidor que ja està en marxa. No crea res nou, sinó que s’uneix al contenidor existent. És l’eina de debugging i inspecció:
docker compose exec db psql -U user myapp # obrir una sessió SQL
docker compose exec api bash # shell dins del contenidor
docker compose exec api cat /app/config.yml # inspeccionar fitxers
La diferència clau entre run i exec: run crea un contenidor nou (el servei no cal que estigui en marxa), exec entra en un que ja existeix (el servei ha d’estar actiu).
Exemple bàsic: API amb base de dades
Un compose.yml mínim per una aplicació web connectada a PostgreSQL:
# compose.yml
name: myapp
services:
api:
build: .
ports:
- "8000:8000"
environment:
- DATABASE_URL=postgresql://user:pass@db:5432/myapp
depends_on:
- db
db:
image: postgres:17-alpine
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
- POSTGRES_DB=myapp
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
Punts clau:
build: .— construeix la imatge des del Dockerfile local en comptes d’usar una imatge preexistentdepends_on— garanteix quedbarrenca abans queapi(però no que estigui llest — això ho resolem a continuació)volumes: postgres_data— les dades de la base de dades persisteixen entre reinicis- L’API accedeix a la base de dades com
db:5432— veure Xarxa entre serveis
Gràcies al volum amb nom, un cicle de docker compose down seguit de docker compose up no perd les dades — el volum sobreviu als contenidors. Compte: docker compose down -v sí elimina els volums, i aquesta acció és irreversible.
Els volums amb nom s’acumulen al disc del host i no s’eliminen automàticament. Convé revisar-los periòdicament:
docker volume ls # llistar volums
docker volume rm <nom> # eliminar un volum concret
docker volume prune # eliminar tots els volums no usats per cap contenidor
docker volume prune és útil per alliberar espai, però cal anar amb compte: un volum d’un projecte aturat apareix com a “no usat” i serà eliminat.
Per veure els detalls d’un volum concret:
docker volume inspect myapp_postgres_data # mostra el punt de muntatge, driver i data de creació
El nom del volum es forma com <projecte>_<nom> — en aquest cas myapp_postgres_data.
Backup amb docker exec
docker exec permet executar comandes dins d’un contenidor en marxa. Això és útil per fer backups sense aturar el servei — pg_dump genera una còpia consistent de la base de dades encara que hi hagi consultes actives:
# Executem pg_dump dins del contenidor i redirigim la sortida al host
docker exec myapp-db-1 pg_dump -U user myapp > backup.sql
# Restaurar el backup (< envia el fitxer com a entrada estàndard, -i activa mode interactiu)
docker exec -i myapp-db-1 psql -U user myapp < backup.sql
El nom myapp-db-1 segueix el patró per defecte de Compose: <projecte>-<servei>-<instància>.
No s’han de copiar directament els fitxers del volum amb PostgreSQL en marxa — els fitxers de dades no estaran en un estat consistent.
Producció: healthchecks i disponibilitat
L’exemple anterior funciona, però té un problema: depends_on només garanteix l’ordre d’inici, no que PostgreSQL estigui realment llest per acceptar connexions. En producció, afegim healthchecks i condicions d’arrencada:
# compose.yml
name: myapp
services:
api:
build: .
ports:
- "8000:8000"
environment:
- DATABASE_URL=postgresql://user:pass@db:5432/myapp
env_file:
- path: .env
required: false
depends_on:
db:
condition: service_healthy
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
db:
image: postgres:17-alpine
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
- POSTGRES_DB=myapp
volumes:
- postgres_data:/var/lib/postgresql/data
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user"]
interval: 10s
timeout: 5s
retries: 5
volumes:
postgres_data:
Què hem afegit respecte l’exemple bàsic:
healthcheck— Docker executa periòdicament el test per saber si el servei funciona (healthy,unhealthyostarting)depends_onambcondition: service_healthy— l’API no arrenca fins que PostgreSQL passi el healthcheckrestart: unless-stopped— reinicia el servei automàticament si falla (excepte si l’atures manualment)env_fileambrequired: false— carrega variables d’un fitxer.envsi existeix, sense fallar si no hi és
Xarxa entre serveis
Compose crea automàticament una xarxa privada per a tots els serveis del fitxer. Dins d’aquesta xarxa, cada servei és accessible pel seu nom: l’API pot connectar-se a PostgreSQL simplement com db:5432, sense necessitat de configurar IPs.
Això és el que permet escriure DATABASE_URL=postgresql://user:pass@db:5432/myapp — db es resol automàticament a l’adreça IP del contenidor de PostgreSQL.
La distinció important és entre ports interns i ports publicats:
- La comunicació entre serveis dins de la xarxa de Compose funciona directament —
dbno necessitaports:perquè l’API hi accedeix per la xarxa interna. - La directiva
ports:publica un port al host, fent el servei accessible des de fora (el navegador,curl, etc.). Només cal publicar els ports dels serveis que han de ser accessibles externament.
Per això a l’exemple, api té ports: "8000:8000" (per rebre peticions externes) però db no en té — la base de dades només ha de ser accessible per l’API, no des del host.
Per inspeccionar les xarxes creades:
docker network ls # Llistar totes les xarxes
docker network inspect <nom_xarxa> # Veure detalls (IPs, contenidors connectats)
Substitució de variables amb valors per defecte
Dins de compose.yml podem usar variables d’entorn amb la sintaxi ${VAR:-default}:
services:
app:
ports:
- "${PORT:-8080}:${PORT:-8080}"
environment:
- PORT=${PORT:-8080}
- HEALTH_ENDPOINT=${HEALTH_ENDPOINT:-/health}
Si PORT no està definit (ni a l’entorn ni a .env), s’usa 8080.
Makefile per simplificar comandes
Un patró comú és usar un Makefile per evitar recordar les comandes llargues de Docker Compose:
docker-build:
docker compose build app
docker-up:
docker compose up -d app
docker-down:
docker compose down
test:
docker compose run --build --rm test
db-up:
docker compose up -d db
db-reset:
docker compose down -v
docker compose up -d db
Així, en lloc de recordar les comandes llargues, n’hi ha prou amb make docker-up o make test.
Distribució d’imatges
Un cop tens la imatge construïda i funcionant localment, el pas següent és distribuir-la perquè altres màquines (servidors de producció, companys d’equip) puguin executar-la. El flux complet d’una imatge al llarg de la seva vida és:
build (local) → push (registre) → pull (servidor) → run (producció)
En un projecte real, aquest flux s’automatitza: el pipeline de CI construeix la imatge, la puja al registre amb una etiqueta que identifica la versió, i el servidor de producció la descarrega i l’executa.
docker tag myapp myapp:1.0.0 # etiqueta la imatge abans de pujar-la
docker push myapp:1.0.0 # puja la imatge al registre
docker pull myapp:1.0.0 # descarrega la imatge en un altre servidor
Docker Hub és adequat per imatges públiques i projectes personals, però en producció les imatges contenen codi propietari, dependències internes i configuracions sensibles que no poden ser públiques. En aquest cas s’utilitzen registres privats, que requereixen autenticació per accedir-hi. El flux de treball és idèntic — push i pull — però les imatges només són accessibles per als equips autoritzats.
Resum de comandes
# === IMATGES ===
docker build -t nom:tag . # Construir imatge
docker build --no-cache -t nom:tag . # Construir sense cache
docker build --build-arg VAR=val -t nom:tag . # Build amb ARG
docker tag nom:tag nom:nova-tag # Etiquetar imatge
docker pull nom:tag # Descarregar imatge
docker push nom:tag # Pujar imatge al registre
docker images # Llistar imatges
# === CONTENIDORS ===
docker run -d -p 8000:8000 nom # Executar contenidor
docker ps # Contenidors en execució
docker ps -a # Tots els contenidors
docker stop <id> # Aturar
docker rm <id> # Eliminar
docker logs -f <id> # Veure logs
docker exec -it <id> bash # Entrar al contenidor
docker inspect <id> # Detalls del contenidor (config, xarxa, estat)
# === VOLUMS ===
docker volume ls # Llistar volums
docker volume rm nom # Eliminar volum
# === XARXA ===
docker network ls # Llistar xarxes
docker network inspect nom # Detalls d'una xarxa
# === COMPOSE ===
docker compose build # Construir imatges dels serveis
docker compose up -d # Aixecar serveis
docker compose up --build # Reconstruir i aixecar
docker compose down # Aturar i eliminar
docker compose down -v # Aturar i eliminar volums
docker compose logs -f # Logs de tots els serveis
docker compose ps # Estat dels serveis
docker compose exec servei bash # Entrar a un servei
docker compose run --rm servei cmd # Executar comando puntual
# === NETEJA ===
docker system prune # Elimina imatges, contenidors i xarxes no usats
# Builds iteratius acumulen imatges "dangling" silenciosament
docker system df # Veure ús d'espai
Annexos
Instal·lació de Docker (Ubuntu/Debian)
# Instal·lar dependencies i afegir clau GPG oficial
sudo apt update
sudo apt install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
# Afegir el repositori a les fonts d'Apt
sudo tee /etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/debian
Suites: $(. /etc/os-release && echo "$VERSION_CODENAME")
Components: stable
Signed-By: /etc/apt/keyrings/docker.asc
EOF
# Instal·lar Docker
sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
# Comprovar que el servei de Docker està actiu
sudo systemctl status docker
# Verificar
docker --version
docker compose version
# Opcional: executar Docker sense sudo
sudo groupadd docker
sudo usermod -aG docker $USER
# Cal tancar sessió i tornar a entrar
# Verificar instal·lació
docker run hello-world
Què passa quan fas docker run?
Un contenidor no és una màquina virtual lleugera — és un procés ordinari del host. No hi ha hipervisor ni kernel convidat. La “màgia” de Docker no està en cap tecnologia secreta, sinó en com orquestra mecanismes que ja existeixen al kernel Linux per crear la il·lusió d’una màquina independent. Vegem què passa pas a pas quan executes docker run nginx:
┌──────────────────────────── HOST ────────────────────────────────────┐
│ │
│ ┌─────────────────── CONTENIDOR (nginx) ─────────────────────────┐ │
│ │ │ │
│ │ Namespaces cgroups │ │
│ │ ┌──────────────────────────┐ ┌──────────────────────────┐ │ │
│ │ │ PID │ NET │ MNT │ UTS │ │ mem: màx 512MB │ │ │
│ │ └──────────────────────────┘ │ cpu: màx 50% │ │ │
│ │ └──────────────────────────┘ │ │
│ │ OverlayFS │ │
│ │ ┌────────────────────────────────────────────────────────┐ │ │
│ │ │ capa escriptura (efímera — desapareix en eliminar) │ │ │
│ │ ├────────────────────────────────────────────────────────┤ │ │
│ │ │ capes de la imatge (read-only, compartides) │ │ │
│ │ └────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ nginx → "soc PID 1, estic sol al sistema" │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
│ nginx real: PID 3847, un procés més entre molts │
│ │
└──────────────────────────────────────────────────────────────────────┘
1. Docker Engine rep la petició. Analitza la comanda, descarrega la imatge si cal, i prepara la configuració del contenidor (ports, volums, variables d’entorn, límits de recursos).
2. Munta el sistema de fitxers (OverlayFS). Les capes de la imatge s’apilen com a lectures-només, i Docker afegeix una capa fina d’escriptura a sobre. Després usa pivot_root per fer que aquest sistema de fitxers sigui el / del contenidor. Per això els canvis dins d’un contenidor no afecten la imatge original — les escriptures van a la capa efímera, i les capes de la imatge romanen intactes.
3. Crea namespaces. El kernel Linux permet crear “vistes” aïllades de recursos del sistema. Docker crea un conjunt de namespaces per al nou procés:
- PID namespace: el procés principal del contenidor és PID 1 dins del contenidor, però des del host és un procés normal amb un PID qualsevol (verificable amb
ps aux | grep nginx). El contenidor no pot veure cap altre procés del host. - Network namespace: el contenidor té les seves pròpies interfícies de xarxa, taula de rutes i adreça IP. Des de dins, sembla que tingui una xarxa dedicada.
- Mount namespace: el contenidor veu el seu propi arbre de muntatge, aïllat del host.
- UTS namespace: el contenidor pot tenir el seu propi hostname.
- User namespace (opcional): permet mapejar l’usuari root dins del contenidor a un usuari sense privilegis al host.
Quan fas docker exec -it web bash, Docker no “entra” al contenidor — el que fa és llançar un nou procés (bash) que s’uneix als namespaces existents del contenidor. Per això bash veu els mateixos processos, la mateixa xarxa i el mateix sistema de fitxers que nginx.
4. Configura cgroups. Els Control Groups limiten quants recursos pot consumir el contenidor: memòria màxima, percentatge de CPU, ample de banda d’I/O. Quan escrius docker run -m 512m, Docker crea un cgroup amb un límit de 512MB — si el procés el supera, el kernel el mata (OOM kill). Sense límits explícits, el contenidor pot consumir tots els recursos del host, cosa que en producció pot fer caure altres serveis.
5. Connecta la xarxa. Docker crea un parell veth (virtual ethernet) — un cable virtual amb dos extrems. Un extrem va dins del network namespace del contenidor, l’altre es connecta al bridge docker0 del host. El mapatge de ports (-p 8080:80) funciona amb regles d’iptables que reenvien el tràfic del port 8080 del host al port 80 dins del namespace de xarxa del contenidor.
client → host:8080
│
iptables/NAT (-p 8080:80)
│
┌─────────────▼──────────────┐
│ bridge docker0 │ 172.17.0.1 (host)
└─────────────┬──────────────┘
│ veth pair (cable virtual)
┌─────────────▼──────────────┐
│ eth0 (contenidor) │ 172.17.0.2
│ nginx escolta :80 │
└────────────────────────────┘
6. Arrenca el procés. Finalment, Docker executa el procés configurat (nginx) que es converteix en el PID 1 del contenidor. Des de la perspectiva del host, és un procés més amb aïllament aplicat. Des de dins, nginx creu que està sol en una màquina dedicada amb el seu propi sistema de fitxers, xarxa i arbre de processos.
No hi ha cap kernel addicional, cap capa de virtualització de maquinari. Només un procés Linux que creu que està sol perquè el kernel li amaga tot el que hi ha fora.
Portàtils ARM en un entorn AMD64
Els portàtils Apple Silicon (M1/M2/M3) i alguns portàtils Linux recents usen arquitectura ARM64 (aarch64). La majoria de companys i els servidors de CI/CD usen AMD64 (x86_64). Les dues arquitectures executen jocs d’instruccions incompatibles: un binari compilat per a AMD64 no s’executa directament en ARM64.
Amb Docker, el problema apareix quan un estudiant ARM construeix una imatge sense especificar plataforma i el company AMD64 intenta executar-la:
WARNING: The requested image's platform (linux/arm64) does not match
the detected host platform (linux/amd64)
exec /usr/bin/app: exec format error
Solució: fixa platform: linux/amd64 al compose.yml
AMD64 és el denominador comú: Docker Desktop sobre Mac ARM i Linux AMD64 el poden executar tots. L’estudiant ARM executarà el contenidor via emulació QEMU (activada automàticament per Docker Desktop), amb comportament idèntic però lleugerament més lent:
# compose.yml
services:
app:
platform: linux/amd64 # ← tots el poden executar
build: .
db:
platform: linux/amd64
image: postgres:17-alpine
I al Dockerfile, fixa la plataforma base explícitament:
FROM --platform=linux/amd64 eclipse-temurin:21-jdk-alpine
Si el projecte necessita imatges natives per a les dues arquitectures, usa buildx per construir una imatge multiplataforma d’una sola tirada:
docker buildx create --name multibuilder --use
docker buildx build \
--platform linux/amd64,linux/arm64 \
--tag registry.example.com/app:latest \
--push .
Docker triarà automàticament la variant correcta en cada màquina en fer docker pull.
Recursos addicionals
- Documentació oficial: docs.docker.com
- Docker Hub: hub.docker.com — Registre d’imatges públiques
- Best practices: docs.docker.com/build/building/best-practices
DevOps
- Introducció
- Principis de disseny
- Aplicació dels principis
- Recapitulació
Introducció
Tradicionalment, les empreses de programari tenien dos equips separats: els desenvolupadors (Dev), que escrivien codi i volien publicar funcionalitats noves ràpidament, i els d’operacions (Ops), que gestionaven els servidors i volien estabilitat. Aquests dos equips sovint treballaven en contra:
- Dev llençava codi “per sobre del mur” a Ops, que havia de fer-lo funcionar en producció.
- Ops rebutjava canvis perquè cada desplegament era un risc d’inestabilitat.
- Els desplegaments es feien manualment, de nit, i amb por.
DevOps és la cultura i el conjunt de pràctiques que elimina aquest mur. L’objectiu és que el cicle entre escriure codi i tenir-lo funcionant en producció sigui ràpid, automatitzat i fiable.
La metodologia Twelve-Factor App defineix dotze principis per construir aplicacions preparades per a aquest model de treball:
- Codebase — un codi font, molts desplegaments
- Dependencies — declarar i aïllar les dependències
- Config — configuració a l’entorn
- Backing services — serveis com a recursos connectables
- Build, release, run — separar compilació, publicació i execució
- Processes — processos sense estat
- Port binding — exportar serveis per port
- Concurrency — escalar amb processos
- Disposability — inici ràpid i aturada elegant
- Dev/prod parity — entorns similars
- Logs — tractats com a fluxos d’esdeveniments
- Admin processes — tasques d’administració com a processos puntuals
Aquests principis s’organitzen naturalment en cinc àrees, que segueixen el cicle de vida d’una aplicació: des de com s’organitza el codi fins a com es manté en producció.
Principis de disseny
A continuació es desenvolupen els dotze factors agrupats en cinc àrees. Cada àrea representa una etapa del cicle: preparar el codi, fer-lo portable, lliurar-lo, executar-lo i operar-lo.
Codi i dependències
Factors I i II
Abans de pensar en servidors o desplegaments, cal que el codi font estigui ben organitzat. Aquests dos principis estableixen la base: un repositori clar i unes dependències que no deixin res a l’atzar.
Codebase
Cada aplicació té un únic repositori de codi font (per exemple, un repositori Git). A partir d’aquest repositori es generen múltiples desplegaments: desenvolupament, staging, producció.
Què passa si no ho fem així?
- Si dues aplicacions comparteixen el mateix repositori, un canvi en una pot trencar l’altra.
- Si una aplicació té múltiples repositoris, és difícil saber quina versió del codi s’està executant.
- Si hi ha codi compartit entre aplicacions, s’ha d’extreure com a llibreria independent amb el seu propi versionat.
Dependencies
Totes les dependències s’han de declarar explícitament en un fitxer del projecte (pom.xml, package.json, requirements.txt…). Mai s’ha de dependre de paquets instal·lats globalment al sistema.
Sense dependències explícites:
- “A la meva màquina funciona!” — però al servidor de producció falta una llibreria que tenies instal·lada sense saber-ho.
- Un company clona el projecte i no li funciona perquè té una versió diferent de Java.
- Actualitzes una dependència global i, de cop, tres projectes deixen de funcionar.
Amb dependències explícites, qualsevol persona (o màquina) pot reconstruir l’entorn exacte de l’aplicació des de zero.
Configuració i serveis externs
Factors III i IV
Un cop tenim el codi i les dependències controlades, el següent problema és: com fem que la mateixa aplicació funcioni en entorns diferents (la teva màquina, un servidor de proves, producció) sense haver de modificar el codi?
Config
La configuració que varia entre desplegaments s’ha d’emmagatzemar en variables d’entorn, mai dins del codi font. Exemples de configuració:
- Credencials de base de dades (
DB_HOST,DB_PASSWORD) - URLs de serveis externs (
API_URL) - Claus d’API (
EMAIL_API_KEY) - Mode de funcionament (
DEBUG=true,LOG_LEVEL=info)
Un bon test: el codi font es podria fer públic en qualsevol moment sense comprometre cap credencial? Si la resposta és no, hi ha configuració que no hauria d’estar al codi.
L’alternativa — fitxers de configuració com config.prod.properties al repositori — és problemàtica: és fàcil cometre errors pujant credencials a Git, i cada entorn nou requereix un fitxer nou.
Backing services
Els serveis auxiliars (bases de dades, cues de missatges, serveis de correu, sistemes de cache) es tracten com a recursos connectables. Per a l’aplicació, no hi ha diferència entre una base de dades PostgreSQL local i una al núvol: només canvia la URL de connexió.
Això vol dir que canviar de proveïdor (per exemple, de MySQL a PostgreSQL, o d’un SMTP local a un servei extern) hauria de requerir només un canvi de configuració (una variable d’entorn), no un canvi de codi.
Aquesta idea connecta directament amb el factor anterior: si la configuració està a l’entorn, connectar un servei diferent és trivial.
Pipeline de lliurament
Factors V i X
Ara que el codi és portable (no depèn de l’entorn), necessitem un procés clar i repetible per portar-lo del repositori fins a producció. Aquí és on DevOps es diferencia més del model tradicional: en lloc de desplegaments manuals i arriscats, el lliurament és un pipeline automatitzat.
Build, release, run
El desplegament es divideix en tres etapes estrictes que mai s’han de barrejar:
- Build: compilar el codi i les dependències en un artefacte executable (un JAR, una imatge Docker, un bundle JavaScript…).
- Release: combinar l’artefacte amb la configuració de l’entorn concret. Cada release té un identificador únic (un timestamp, un hash de commit, un número de versió).
- Run: executar la release a l’entorn de destí.
Per què separar-les? Perquè si algú edita codi directament al servidor de producció (saltant-se el build), no hi ha manera de saber què s’està executant, ni de tornar enrere si alguna cosa falla. La separació garanteix traçabilitat i reproduïbilitat.
Dev/prod parity
Els entorns de desenvolupament, staging i producció han de ser tan semblants com sigui possible. Les diferències entre entorns són una font habitual de problemes:
- Desenvolupes amb SQLite però producció utilitza PostgreSQL → consultes SQL que fallen.
- A la teva màquina el sistema de fitxers no distingeix majúscules, però a Linux sí → fitxers no trobats.
- Proves amb dades petites localment, però en producció les consultes són lentes amb milions de registres.
Docker i la infraestructura com a codi (Infrastructure as Code) permeten replicar l’entorn de producció localment, reduint aquestes sorpreses.
Execució i escalabilitat
Factors VI, VII i VIII
L’aplicació ja és al servidor. Com s’executa? I què passa quan un sol servidor no és suficient per atendre tota la demanda? Aquests principis defineixen com dissenyar aplicacions que puguin créixer.
Processes
L’aplicació s’executa com un o més processos sense estat (stateless). Això vol dir que el procés no guarda res entre peticions: qualsevol dada que hagi de persistir va a un backing service (base de dades, cache, etc.).
Per què és important? Imagina una botiga en línia on l’usuari afegeix productes al carretó. Si el carretó es guarda a la memòria del procés:
- Si el procés es reinicia, l’usuari perd el carretó.
- Si hi ha dos processos (per repartir la càrrega), l’usuari pot veure el carretó en un i no en l’altre.
Si el carretó es guarda a una base de dades o a Redis, qualsevol procés pot servir qualsevol petició. Això és la base de l’escalabilitat horitzontal.
Port binding
L’aplicació és autocontinguda i exposa els seus serveis a través d’un port de xarxa. No depèn d’un servidor web extern per funcionar.
En el model tradicional, desplegaves un fitxer WAR dins d’un Tomcat compartit amb altres aplicacions. Amb port binding, cada aplicació inclou el seu propi servidor HTTP (Tomcat embegut, Express, Uvicorn…) i escolta en un port. Això simplifica el desplegament i l’aïllament entre aplicacions.
Concurrency
Quan l’aplicació necessita gestionar més càrrega, hi ha dues estratègies:
- Escalar verticalment: posar un servidor més gran (més CPU, més RAM). Té un límit físic i és car.
- Escalar horitzontalment: executar múltiples instàncies de l’aplicació. No té límit pràctic si l’aplicació és stateless.
A més, cada tipus de treball pot escalar independentment: pots tenir 10 processos servint peticions web però només 2 processant tasques en segon pla.
Operació i robustesa
Factors IX, XI i XII
L’aplicació funciona en producció. Ara cal garantir que sigui observable (podem saber què passa), robusta (resisteix errors) i mantenible (podem fer canvis operacionals sense parar-la).
Disposability
Els processos s’han de poder iniciar en segons i aturar de forma elegant (graceful shutdown): completant les peticions en curs i alliberant recursos abans de tancar-se.
Per què importa?
- Desplegaments: cada vegada que publiques una nova versió, els processos antics s’aturen i els nous s’inicien. Si triga minuts, el desplegament és lent i arriscat.
- Escalat elàstic: si arriba un pic de tràfic, necessites instàncies noves ara, no d’aquí 5 minuts.
- Robustesa: si un procés falla, el sistema el pot reiniciar immediatament sense impacte visible.
Logs
L’aplicació ha d’escriure els events com un flux i no preocupar-se de on acaben: ni gestionar fitxers, ni rotacions, ni emmagatzematge. L’entorn d’execució s’encarrega de recollir, agregar i emmagatzemar els logs.
Això no vol dir utilitzar print. Cal distingir dues decisions diferents:
- Com escriure logs (decisió de codi): utilitzar un logger (
loggingen Python,java.util.loggingen Java) en lloc deprint. El logger dóna estructura, nivells (DEBUG, INFO, ERROR), format consistent i context (timestamp, nom del mòdul). - On van els logs (decisió de desplegament): el logger es pot configurar per escriure a consola, a fitxer, o a tots dos. El factor XI diu que en producció, especialment amb contenidors, la sortida hauria d’anar a
stdouti deixar que l’entorn els gestioni.
Per què no escriure directament a fitxers en producció?
- Si tens 10 instàncies de l’aplicació, tens 10 fitxers de log en 10 màquines diferents. Com els consultes de manera unificada?
- Qui gestiona la rotació dels fitxers perquè no omplin el disc?
- Com busques un error que va passar ahir a les 3 de la matinada?
Amb logs a stdout, eines com ELK Stack o Loki els recullen automàticament, els indexen i permeten buscar-hi. En desenvolupament local, en canvi, és perfectament raonable configurar el logger perquè escrigui també a un fitxer.
Admin processes
Les tasques d’administració (migracions de base de dades, scripts de correcció de dades, consola interactiva) s’han d’executar com a processos puntuals (one-off), utilitzant el mateix codi i la mateixa configuració que l’aplicació en producció.
Això vol dir: no connectar-se per SSH al servidor i executar SQL directament a la base de dades. En lloc d’això, escriure un script de migració que formi part del codi, estigui versionat al repositori, i s’executi com un procés més.
Aplicació dels principis
Els dotze factors són principis de disseny. Aplicar-los a la pràctica requereix eines concretes i processos ben definits. Aquesta secció segueix el cicle de vida d’una aplicació en quatre etapes: gestionar el codi amb Git, empaquetar-lo en un entorn portable amb Docker, automatitzar el seu lliurament amb pipelines de CI/CD, i operar-lo en producció amb observabilitat.
Gestió del codi
Codebase
Git implementa directament el primer factor: els diferents desplegaments (desenvolupament, staging, producció) es gestionen amb branques i etiquetes dins d’un únic repositori, no amb còpies de carpetes.
Build, release, run
Git proporciona identificadors únics per a cada release. Un commit hash (a3f8b21) o una etiqueta (v1.2.3) identifiquen exactament quin codi es va construir i desplegar:
git tag -a v1.2.3 -m "Release 1.2.3: fix login timeout"
git push origin v1.2.3
Això permet saber sempre què s’executa en producció i tornar enrere si cal (git checkout v1.2.2). Sense aquest versionat, la pregunta “quin codi hi ha desplegat?” no té resposta clara.
Config
El fitxer .gitignore és la primera línia de defensa per mantenir la configuració fora del codi. Fitxers com .env, credentials.json o config.local.properties mai han d’arribar al repositori:
.env
*.local.properties
credentials/
Si un secret es puja accidentalment a Git, queda a l’historial per sempre (fins i tot si s’esborra al commit següent). Per això és crític ignorar-los des del principi.
Admin processes
Els scripts d’administració (migracions, correccions de dades, tasques de manteniment) han de formar part del repositori, al costat del codi de l’aplicació. Això garanteix que:
- Estan versionats: es pot saber quina migració es va aplicar i quan.
- Són reproduïbles: es poden executar en qualsevol entorn.
- Són revisables: passen pel mateix procés de revisió que el codi (pull requests).
projecte/
├── src/
├── migrations/
│ ├── 001_create_users.sql
│ ├── 002_add_email_column.sql
│ └── 003_fix_duplicates.py
└── scripts/
└── cleanup_old_sessions.py
Git Hooks
Git permet executar scripts automàticament en moments clau del flux de treball. Aquests scripts s’anomenen hooks i són el mecanisme bàsic per automatitzar el pipeline de lliurament (Build, release, run).
Hi ha hooks locals (s’executen a la màquina del desenvolupador) i hooks al servidor (s’executen quan el repositori remot rep codi):
pre-commit(local): s’executa abans de crear un commit. Pot validar el format del codi, executar un linter o verificar que no hi ha secrets al codi. Si el script retorna un error, el commit es rebutja.pre-receive(servidor): s’executa quan algú fagit pushal repositori remot, abans d’acceptar els canvis. Pot rebutjar el push si el codi no passa els tests o no compleix les regles del projecte. És la porta d’entrada (quality gate) al repositori compartit.post-receive(servidor): s’executa després d’acceptar els canvis. És el moment natural per disparar accions automàtiques: construir la imatge Docker, executar els tests d’integració, desplegar a staging.
La idea fonamental és: un git push pot desencadenar tot el pipeline de lliurament sense intervenció manual. El desenvolupador puja codi, i la cadena build → test → deploy s’executa automàticament.
Aquesta és la base conceptual del que fan les plataformes de CI/CD (GitLab CI, Jenkins…): escoltar events de Git i executar pipelines en resposta. Però el mecanisme original són els hooks de Git, i en entorns autogestionats (amb Gitolite, per exemple) els hooks pre-receive i post-receive són tot el que cal per implementar un pipeline complet.
Empaquetament i entorn
Docker no és un requisit dels dotze factors, però és l’eina que els fa naturals. Sense contenidors, complir aquests principis requereix disciplina manual; amb contenidors, molts es compleixen per defecte.
Dependencies
Un Dockerfile és, per definició, una declaració explícita de totes les dependències. El contenidor resultant és un entorn aïllat i complet: no depèn de res instal·lat a la màquina host.
FROM python:3.12-slim
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
Qualsevol persona que executi docker build obtindrà exactament el mateix entorn. El problema de “a la meva màquina funciona” desapareix.
Config
Docker utilitza variables d’entorn de forma nativa. Es poden passar al moment d’executar el contenidor, sense tocar el codi ni la imatge:
docker run -e DB_HOST=postgres -e DB_PASSWORD=secret myapp
O amb Docker Compose:
services:
api:
image: myapp
environment:
- DB_HOST=postgres
- DB_PASSWORD=secret
La mateixa imatge serveix per a desenvolupament, staging i producció. Només canvien les variables.
Variables no sensibles (com LOG_LEVEL o APP_PORT) poden estar al fitxer docker-compose.yml del repositori sense cap problema. Però els secrets (contrasenyes, claus d’API, tokens) no han d’estar mai ni al codi ni a la imatge. D’on surten, doncs? De la infraestructura: en desenvolupament local, d’un fitxer .env ignorat per .gitignore; en entorns automatitzats, del magatzem de secrets de la plataforma de CI/CD (per exemple, les CI/CD Variables de GitLab), que els injecta com a variables d’entorn durant l’execució del pipeline. En tots els casos, els secrets s’injecten en el moment d’executar el contenidor — mai es graven a la imatge durant el build.
Backing services
Docker Compose permet definir backing services com a contenidors adjacents. Canviar de servei és canviar una línia de configuració:
services:
api:
image: myapp
environment:
- DATABASE_URL=postgresql://db:5432/app
db:
image: postgres:16
Afegir un Redis per a cache, o substituir el motor de base de dades? Només cal afegir o canviar un servei al fitxer Compose i actualitzar la variable d’entorn.
Build, release, run
Les tres etapes es mapegen directament a comandes Docker:
| Etapa | Comanda | Resultat |
|---|---|---|
| Build | docker build -t myapp . | Imatge amb codi + dependències |
| Release | docker tag myapp myapp:v1.2.3 | Imatge etiquetada amb versió |
| Run | docker run myapp:v1.2.3 | Contenidor en execució |
Cada imatge és immutable. No es pot editar codi dins d’un contenidor en producció (si es fa, els canvis es perden al reiniciar). Això força la separació d’etapes.
Processes
Els contenidors són efímers per disseny: es poden destruir i recrear en qualsevol moment. Si el contenidor guarda estat a la seva capa d’escriptura, aquest estat es perd.
Això obliga a emmagatzemar les dades persistents fora del contenidor (en volums o en backing services), que és exactament el que demana el principi de Processes.
Port binding
Cada contenidor exposa el seu servei a través d’un port, mapeig que es defineix explícitament:
docker run -p 8080:8080 myapp
No hi ha servidor web compartit. Cada contenidor és autocontingut amb el seu propi procés escoltant al port.
Concurrency
Escalar és executar més contenidors:
docker compose up --scale api=5
Cinc instàncies idèntiques del servei, cadascuna escoltant al seu port, darrere d’un balancejador de càrrega. Això només funciona si l’aplicació és stateless (Processes).
Dev/prod parity
La mateixa imatge s’executa a tot arreu: el portàtil del desenvolupador, el servidor de CI, staging i producció. Les diferències entre entorns — que al principi de Dev/prod parity hem vist que són font d’errors — es redueixen a les variables de configuració (Config).
Disposability
Els contenidors s’inicien en segons. Quan cal aturar-los, Docker envia un senyal SIGTERM al procés principal; si no respon dins d’un temps límit, envia SIGKILL. L’aplicació ha de capturar SIGTERM per fer una aturada elegant (completar peticions en curs, tancar connexions).
Logs
Per defecte, Docker captura tot el que el procés escriu a stdout i stderr. Es pot consultar amb docker logs o redirigir a sistemes d’agregació mitjançant log drivers. L’aplicació utilitza un logger per escriure amb estructura i nivells, però la configuració de producció envia la sortida a stdout — i Docker s’encarrega de la resta.
Automatització del lliurament
Fins ara hem vist com gestionar el codi (Git) i com empaquetar-lo (Docker). Però entre un git push i una aplicació funcionant en producció hi ha molts passos: compilar, executar tests, construir la imatge, desplegar-la… Si aquests passos es fan manualment, són lents, propensos a errors i difícils de reproduir. L’automatització converteix tot aquest procés en un pipeline: una seqüència de passos que s’executa automàticament cada vegada que algú puja codi.
El mecanisme que dispara el pipeline són els hooks de Git (explicats anteriorment). Les plataformes de CI/CD (GitLab CI, Jenkins…) escolten events de Git i executen els passos definits.
Integració contínua (CI)
Sense integració contínua, cada desenvolupador treballa en la seva branca durant dies o setmanes. El dia que tothom intenta fusionar els canvis — el temut merge day — apareixen conflictes, tests que fallen i funcionalitats que es trenquen mútuament.
La integració contínua evita aquest problema: cada desenvolupador integra els seus canvis freqüentment (idealment, cada dia), i el sistema verifica automàticament que tot segueix funcionant:
- Lint: verificar l’estil i format del codi
- Tests: executar els tests unitaris i d’integració
- Build: compilar i construir l’artefacte (imatge Docker, JAR, bundle…)
Si qualsevol pas falla, el pipeline s’atura i el desenvolupador rep un avís. El codi no avança fins que tots els passos passen. Això és una quality gate: el pipeline protegeix el repositori de codi defectuós.
La idea clau és que els problemes es detecten en minuts, no dies o setmanes després, quan ja ningú recorda què va canviar.
Lliurament continu (CD)
Un cop el codi passa la CI, el pipeline pot continuar automàticament cap als entorns de desplegament:
- Desplegar a staging: un entorn idèntic a producció on es poden fer proves addicionals (proves d’acceptació, proves de rendiment…)
- Desplegar a producció: automàticament o amb una aprovació manual
La diferència entre lliurament continu (continuous delivery) i desplegament continu (continuous deployment) és l’últim pas: en el lliurament continu, el desplegament a producció requereix una aprovació manual; en el desplegament continu, és completament automàtic. La majoria d’equips comencen amb lliurament continu i avancen cap a desplegament continu a mesura que guanyen confiança en els seus tests i pipeline.
El flux complet és:
git push → lint → test → build → staging → [aprovació] → producció
Un desplegament que abans requeria hores de treball manual i coordinació entre equips es redueix a un git push i uns minuts d’espera.
Desplegament de la infraestructura
El pipeline no només desplega codi. També pot configurar servidors, instal·lar dependències i preparar l’entorn.
Tradicionalment, configurar un servidor era un procés manual: connectar-se per SSH, instal·lar paquets, editar fitxers de configuració… Si el servidor fallava, calia repetir tot el procés de memòria (o amb sort, seguint unes notes). I si calia preparar un segon servidor idèntic, les diferències apareixien inevitablement.
Eines de gestió de configuració com Ansible permeten definir l’estat desitjat d’un servidor en fitxers versionats al repositori:
- Quin sistema operatiu i paquets han d’estar instal·lats
- Quins serveis han d’estar actius
- Quines variables d’entorn s’han de configurar
Aquesta idea — que la infraestructura es defineix com a codi, es versiona i s’aplica automàticament — s’anomena Infrastructure as Code (IaC). Garanteix que tots els entorns es configuren de manera idèntica, i que qualsevol canvi d’infraestructura passa pel mateix procés de revisió que el codi de l’aplicació.
Operació en producció
L’aplicació funciona en producció. Tot va bé… o no? Sense mecanismes d’observació, l’equip només descobreix els problemes quan un usuari es queixa. I a vegades, un problema petit (una consulta SQL que es fa lenta) es converteix en un problema gran (el sistema cau) perquè ningú el va detectar a temps.
L’observabilitat és la capacitat de saber què passa dins d’un sistema des de fora, sense haver d’accedir als servidors ni depurar codi en producció. Es basa en quatre pilars:
Mètriques
Les mètriques són valors numèrics que es recullen periòdicament i permeten veure tendències. Les quatre mètriques fonamentals per a qualsevol servei web són:
- Latència: quant triga una petició? Es mesura en percentils (p50, p95, p99) perquè la mitjana amaga els casos extrems. Si el p50 és de 100ms però el p99 és de 5s, un de cada cent usuaris té una experiència molt dolenta.
- Throughput: quantes peticions per segon atén el sistema?
- Errors: quin percentatge de peticions fallen? (anomenat error rate)
- Recursos: quanta CPU, memòria i disc s’utilitzen?
Les mètriques responen la pregunta “com va el sistema ara?” i permeten detectar degradacions abans que els usuaris les notin. Per exemple, si la latència p95 ha pujat de 200ms a 800ms en les últimes dues hores, alguna cosa està canviant — encara que els usuaris no s’hagin queixat.
Logs
L’aplicació utilitza un logger que escriu a stdout i l’entorn els recull (com hem vist a Empaquetament i entorn). Però per ser útils en producció amb múltiples instàncies, els logs haurien de ser estructurats (en format JSON, per exemple), perquè les eines d’agregació els puguin indexar i buscar-hi:
{"timestamp": "2025-03-15T10:23:45Z", "level": "ERROR", "service": "api", "message": "Connection refused", "host": "db-primary"}
Amb logs estructurats, es pot buscar “tots els errors del servei api de les últimes 2 hores” en segons, fins i tot amb 10 instàncies escrivint en paral·lel. Sense estructura, els logs són text lliure difícil de filtrar i analitzar automàticament.
Health checks
Un health check és un endpoint que indica si l’aplicació està preparada per rebre peticions. Normalment és una URL simple:
GET /health → 200 OK
GET /health → 503 Service Unavailable
Per què no n’hi ha prou amb saber que el procés existeix? Perquè un contenidor pot estar en execució però no funcionar correctament — per exemple, perquè ha perdut la connexió amb la base de dades, o perquè s’ha quedat sense memòria interna. El health check verifica que l’aplicació realment pot atendre peticions.
Qui consulta aquest endpoint?
- Docker: pot reiniciar un contenidor que falla el health check
- Balancejadors de càrrega: deixen d’enviar tràfic a instàncies no saludables
- Pipelines de CI/CD: verifiquen que el desplegament ha funcionat abans de donar-lo per bo
Alertes
Les mètriques i els health checks generen dades contínuament, però ningú pot estar mirant gràfics les 24 hores del dia. Les alertes automàtiques defineixen condicions que, quan es compleixen, envien una notificació a l’equip:
- La latència p99 supera els 2 segons durant més de 5 minuts
- El percentatge d’errors supera l’1%
- Un health check falla 3 vegades consecutives
Un bon sistema d’alertes avisa dels problemes reals sense generar fatiga d’alertes — si l’equip rep 50 alertes al dia, acabarà ignorant-les totes.
Recapitulació
DevOps connecta el desenvolupament amb les operacions a través d’un cicle continu: el codi es gestiona amb Git, s’empaqueta amb Docker, es lliura automàticament amb un pipeline de CI/CD, i s’opera amb observabilitat. Els dotze factors són els principis que guien cada etapa d’aquest cicle — i les eines que hem vist són les que els fan pràctics.
Infrastructure as Code
- Introducció
- Conceptes clau
- Aprovisionament: Terraform
- Gestió de configuració: Ansible
- Patrons d’ús
- Gestió de secrets
- Integració amb CI/CD
- Referències
Introducció
Tradicionalment, configurar un servidor era un procés manual: connectar-se per SSH, instal·lar paquets, editar fitxers de configuració, reiniciar serveis… Si tot anava bé, el servidor funcionava. Si no, calia repetir el procés — sovint de memòria o seguint unes notes desactualitzades.
Aquest model té problemes greus:
- Configuration drift: amb el temps, els servidors acumulen canvis manuals que ningú ha documentat. Dos servidors que haurien de ser idèntics acaben sent diferents.
- Snowflake servers: cada servidor és únic i irreproduïble. Si falla, reconstruir-lo és un projecte en si mateix.
- Falta de traçabilitat: no hi ha registre de qui va canviar què, ni quan, ni per què.
- Errors humans: un pas oblidat, una comanda equivocada, un fitxer sobreescrit… i el servei cau.
La Infrastructure as Code (IaC) resol aquests problemes aplicant al món de la infraestructura les mateixes pràctiques que ja utilitzem per al codi: la infraestructura es descriu en fitxers de text, es versiona amb Git, es revisa amb pull requests i s’aplica automàticament mitjançant eines.
A DevOps hem vist que la infraestructura es pot definir com a codi. Aquest document desenvolupa aquesta idea en profunditat.
Els beneficis són directes:
- Reproduïbilitat: qualsevol persona (o pipeline) pot reconstruir un entorn des de zero executant el codi.
- Consistència: tots els entorns es configuren de manera idèntica, eliminant el configuration drift.
- Traçabilitat: cada canvi d’infraestructura queda registrat a l’historial de Git.
- Col·laboració: els canvis d’infraestructura passen pel mateix flux de revisió que el codi de l’aplicació.
IaC es divideix en dos grans pilars:
- Aprovisionament (provisioning): crear els recursos — màquines virtuals, xarxes, bases de dades al núvol. L’eina de referència és Terraform.
- Gestió de configuració (configuration management): configurar el que ja existeix — instal·lar paquets, desplegar aplicacions, gestionar serveis. L’eina de referència és Ansible.
Una analogia: l’aprovisionament és construir la casa; la gestió de configuració és moblar-la i connectar-hi els serveis.
Aquests dos pilars operen a nivell d’infraestructura: servidors, xarxes, sistemes operatius. Però hi ha un segon nivell — el de l’aplicació — on Docker Compose fa un paper equivalent: aprovisiona (crea contenidors, xarxes, volums) i configura (variables d’entorn, ports, dependències) els serveis de l’aplicació dins d’un servidor ja preparat.
| Nivell | Aprovisionament | Configuració |
|---|---|---|
| Infraestructura | Terraform crea VMs, xarxes | Ansible instal·la paquets, configura serveis |
| Aplicació | Compose crea contenidors, volums, xarxes | Compose configura variables, ports, dependències |
Un escenari molt habitual combina les tres eines — Terraform, Ansible i Docker Compose — en un flux natural (tot i que no és l’únic possible, com veurem a la secció de patrons d’ús):
Terraform crea les màquines i n’exporta les IPs. Ansible utilitza aquestes IPs per connectar-s’hi, configurar-les i desplegar-hi l’aplicació amb Docker Compose. Tot definit en fitxers, versionat a Git, i executable amb un pipeline.
Conceptes clau
Abans d’entrar a les eines concretes, cal entendre tres conceptes fonamentals que travessen tota la disciplina d’IaC.
Declaratiu vs imperatiu
Hi ha dues maneres de descriure la infraestructura:
-
Imperativa: una seqüència de comandes que s’executen en ordre. Com arribar a l’estat desitjat.
apt update apt install -y nginx systemctl start nginx systemctl enable nginx -
Declarativa: una descripció de l’estat final desitjat. Què volem, no com arribar-hi.
- name: Nginx instal·lat i actiu apt: name: nginx state: present - name: Servei nginx actiu service: name: nginx state: started enabled: true
La majoria d’eines d’IaC (Terraform, Ansible, Docker Compose) segueixen el model declaratiu: l’eina s’encarrega de calcular els passos necessaris per arribar a l’estat descrit.
Idempotència
Una operació és idempotent si executar-la múltiples vegades produeix el mateix resultat que executar-la un cop. Si Ansible detecta que nginx ja està instal·lat, no fa res. Si detecta que falta, l’instal·la.
Això és fonamental perquè permet reexecutar el codi d’infraestructura amb seguretat: si alguna cosa falla a mitges, es pot tornar a executar sense por de duplicar o trencar res.
Infraestructura mutable vs immutable
- Mutable: els servidors es modifiquen in place. Ansible actualitza paquets, canvia configuracions, reinicia serveis sobre una màquina existent.
- Immutable: en lloc de modificar un servidor, se’n crea un de nou amb la configuració actualitzada i es destrueix l’antic. Docker segueix aquest model: no es modifiquen contenidors, es recreen.
| Concepte | Exemple mutable | Exemple immutable |
|---|---|---|
| Actualització | Ansible actualitza nginx al servidor | Es construeix una nova imatge Docker amb nginx actualitzat |
| Rollback | Ansible reverteix la configuració | Es redesplega la imatge anterior |
| Risc | Acumulació de canvis no previstos | Cal reconstruir la imatge sencera |
A la pràctica, la majoria d’entorns combinen ambdós models: els servidors es gestionen de forma mutable amb Ansible, però les aplicacions s’hi despleguen com a contenidors immutables amb Docker.
Aprovisionament: Terraform
Terraform és l’eina d’aprovisionament per excel·lència. Permet crear i gestionar recursos — màquines virtuals, xarxes, DNS, bases de dades gestionades — mitjançant fitxers de configuració declaratius. Funciona tant amb plataformes de virtualització autogestionades (Proxmox, OpenStack) com amb proveïdors de núvol públic: qualsevol sistema que exposi una API pot tenir un provider de Terraform.
Funcionament bàsic
Terraform utilitza un llenguatge propi anomenat HCL (HashiCorp Configuration Language). La configuració es defineix en fitxers .tf:
provider "proxmox" {
endpoint = var.proxmox_url
api_token = var.proxmox_token
}
resource "proxmox_virtual_environment_vm" "web" {
name = "web-server"
node_name = "pve1"
clone {
vm_id = 9000 # template Ubuntu 22.04
}
cpu {
cores = 2
}
memory {
dedicated = 2048
}
network_device {
bridge = "vmbr0"
}
}
Conceptes essencials
- Providers: connectors que parlen amb les APIs de plataformes externes (Proxmox, OpenStack, proveïdors de núvol, etc.).
- Resources: els recursos que es creen (
proxmox_virtual_environment_vm,openstack_compute_instance, etc.). Terraform analitza les dependències entre recursos i els crea en l’ordre correcte — si una VM necessita una xarxa, primer crea la xarxa. - State: Terraform manté un fitxer d’estat (
terraform.tfstate) que registra quins recursos existeixen i les seves propietats. Gràcies a aquest registre, Terraform pot calcular la diferència entre l’estat actual i el desitjat, i aplicar només els canvis necessaris — sense tocar el que ja és correcte. - Cicle plan/apply:
terraform planmostra els canvis que s’aplicaran sense executar-los, permetent revisar i validar abans d’actuar. Només quan es confirma ambterraform applys’executen els canvis reals. Això redueix el risc d’errors en producció.
| Enfocament | Procés |
|---|---|
| Manual | Entrar al panell web → clicar “Crear servidor” → configurar opcions → repetir per cada recurs |
| Terraform | Escriure .tf → terraform plan → revisar → terraform apply → tots els recursos creats |
Altres enfocaments d’aprovisionament
Terraform no és l’única manera d’aprovisionar infraestructura. Depenent del context, es poden utilitzar altres enfocaments:
- Ansible com a aprovisionador: Ansible també pot crear recursos — contenidors LXC, instàncies al núvol (amb mòduls com
community.general.lxc_container), etc. Això elimina la necessitat d’una eina addicional, però perd els avantatges de Terraform: el fitxer d’estat, el cicle plan/apply, i la gestió automàtica de dependències entre recursos. - Imatges preconfigurades: en lloc de crear una màquina buida i configurar-la després, es pot construir una imatge base (amb Packer, per exemple) que ja inclou el sistema operatiu configurat, Docker instal·lat, i les eines necessàries. Terraform crea instàncies a partir d’aquesta imatge, reduint el treball d’Ansible al mínim. Això s’apropa al model d’infraestructura immutable.
A la secció de Patrons d’ús es veuran exemples concrets d’aquests enfocaments.
Gestió de configuració: Ansible
Ansible és l’eina principal de gestió de configuració. Permet definir l’estat desitjat dels servidors — quins paquets han d’estar instal·lats, quins serveis actius, quins fitxers configurats — i aplicar-lo automàticament.
Què és Ansible
La filosofia d’Ansible es basa en tres principis:
- Agentless: no cal instal·lar cap programari als servidors gestionats. Ansible es connecta per SSH des d’una màquina de control (el portàtil del desenvolupador o un servidor de CI/CD) i executa les tasques remotament.
- Declaratiu: es descriu l’estat desitjat, no els passos per arribar-hi.
- Idempotent: executar el mateix codi dues vegades no canvia res si l’estat ja és el correcte.
Per què Ansible interessa als desenvolupadors? Perquè utilitza YAML — el mateix format que Docker Compose i els pipelines de CI/CD —, no requereix agents, i permet automatitzar el desplegament de les aplicacions que escriuen.
Inventari
L’inventari és la llista de màquines que Ansible gestiona, organitzades en grups. Es defineix en un fitxer YAML:
all:
children:
webservers:
hosts:
web1:
ansible_host: 192.168.1.10
web2:
ansible_host: 192.168.1.11
databases:
hosts:
db1:
ansible_host: 192.168.1.20
Els grups permeten aplicar configuracions diferents a cada tipus de servidor. Les variables específiques de cada grup o host es defineixen en directoris separats:
project/
├── inventory.yml
├── group_vars/
│ ├── all.yml # Variables per a tots els hosts
│ ├── webservers.yml # Variables per als servidors web
│ └── databases.yml # Variables per a les bases de dades
└── host_vars/
└── web1.yml # Variables específiques de web1
En entorns amb moltes màquines o infraestructura canviant, mantenir l’inventari a mà no escala. Ansible suporta inventaris dinàmics: plugins que consulten l’API de la plataforma i generen la llista de màquines automàticament. Per exemple, amb el plugin de Proxmox, Ansible pot descobrir totes les VMs amb una etiqueta concreta (role: webserver) i afegir-les al grup corresponent. Si Terraform crea o destrueix màquines, l’inventari dinàmic s’actualitza sol a la propera execució.
Quan s’utilitza Terraform per aprovisionar, una opció senzilla és passar les IPs directament a Ansible:
# Terraform genera les IPs, Ansible les consumeix
terraform output -json ips | jq -r '.[]' > hosts.txt
ansible-playbook -i hosts.txt playbook.yml
Amb inventaris dinàmics, ni tan sols cal aquest pas intermedi: Ansible consulta directament la plataforma.
Playbooks i tasques
Un playbook és un fitxer YAML que descriu un conjunt de tasques a aplicar a un grup de hosts. L’estructura és: plays (quins hosts + quines tasques) i tasks (accions individuals que utilitzen mòduls).
---
- name: Configurar servidor web
hosts: webservers
become: true
tasks:
- name: Instal·lar nginx
apt:
name: nginx
state: present
update_cache: true
- name: Copiar configuració
template:
src: nginx.conf.j2
dest: /etc/nginx/sites-available/default
notify: Reiniciar nginx
- name: Activar el servei
service:
name: nginx
state: started
enabled: true
handlers:
- name: Reiniciar nginx
service:
name: nginx
state: restarted
Cada tasca utilitza un mòdul d’Ansible: apt per gestionar paquets, template per generar fitxers, service per gestionar serveis, copy per copiar fitxers, docker_compose per gestionar contenidors, etc.
La idempotència es veu a cada tasca: apt: state=present instal·la nginx només si no està instal·lat. Si ja ho està, Ansible reporta ok i passa a la següent tasca. Executar el playbook dues vegades produeix el mateix resultat.
La sortida d’Ansible mostra l’estat de cada tasca:
- ok: l’estat ja era el correcte, no s’ha canviat res
- changed: s’ha aplicat un canvi
- failed: la tasca ha fallat
Handlers
Els handlers són tasques que només s’executen quan una altra tasca els notifica. El cas d’ús clàssic: reiniciar un servei només quan el seu fitxer de configuració canvia.
Al playbook anterior, la tasca “Copiar configuració” inclou notify: Reiniciar nginx. Si Ansible detecta que el fitxer ha canviat, marca el handler per executar-se. Si el fitxer ja era idèntic, el handler no s’executa.
Característiques importants:
- Els handlers s’executen un cop al final del play, encara que siguin notificats múltiples vegades.
- Això evita reinicis innecessaris: si tres tasques modifiquen la configuració de nginx, el servei només es reinicia un cop.
Templates amb Jinja2
Les templates permeten generar fitxers de configuració dinàmicament, substituint variables pels seus valors. Ansible utilitza el motor de templates Jinja2.
Un fitxer nginx.conf.j2:
server {
listen {{ http_port | default(80) }};
server_name {{ server_name }};
location / {
proxy_pass http://localhost:{{ app_port }};
}
}
Quan Ansible processa aquesta template amb les variables http_port: 8080, server_name: example.com i app_port: 3000, genera:
server {
listen 8080;
server_name example.com;
location / {
proxy_pass http://localhost:3000;
}
}
Jinja2 suporta condicionals ({% if %}) i bucles ({% for %}), cosa que permet generar configuracions complexes:
{% for backend in app_backends %}
upstream {{ backend.name }} {
server {{ backend.host }}:{{ backend.port }};
}
{% endfor %}
Variables i jerarquia de precedència
Les variables es poden definir en molts llocs. Ansible aplica una jerarquia de precedència per determinar quin valor s’utilitza quan una variable es defineix en més d’un lloc.
De menys a més prioritat (simplificat):
| Prioritat | Origen |
|---|---|
| 1 (més baixa) | defaults/main.yml del rol |
| 2 | Variables d’inventari (group_vars/, host_vars/) |
| 3 | vars: al playbook |
| 4 | vars/main.yml del rol |
| 5 (més alta) | Línia de comandes (-e "variable=valor") |
La bona pràctica és:
- Valors per defecte als
defaults/dels rols (es poden sobreescriure fàcilment) - Configuració per entorn a
group_vars/(producció, staging) - Configuració específica per host a
host_vars/ - Sobreescriptures puntuals a la línia de comandes
Rols
Els rols són el mecanisme d’Ansible per organitzar i reutilitzar codi, de manera similar a les funcions o mòduls en programació. Un rol encapsula totes les tasques, handlers, templates i variables necessaris per configurar un component concret.
Estructura estàndard d’un rol:
roles/
└── nginx/
├── tasks/
│ └── main.yml # Tasques principals
├── handlers/
│ └── main.yml # Handlers
├── templates/
│ └── nginx.conf.j2 # Templates Jinja2
├── files/
│ └── index.html # Fitxers estàtics
├── defaults/
│ └── main.yml # Variables per defecte
└── meta/
└── main.yml # Dependències del rol
Utilitzar un rol al playbook és senzill:
---
- name: Configurar servidor web
hosts: webservers
become: true
roles:
- common
- nginx
Ansible executa els rols en ordre. Cada rol és independent i autocontingut, cosa que permet reutilitzar-lo en projectes diferents.
Ansible Galaxy és el repositori de la comunitat on es publiquen rols reutilitzables. Per exemple, geerlingguy.docker instal·la i configura Docker — en lloc d’escriure les tasques des de zero, es pot aprofitar el treball de la comunitat.
Altres enfocaments de configuració
Ansible configura servidors de forma mutable: modifica l’estat d’una màquina existent. Però no és l’únic enfocament possible:
- Contenidors Docker: en lloc de configurar un servidor amb Ansible perquè executi nginx, es desplega un contenidor nginx ja configurat. La configuració es trasllada al
Dockerfilei alcompose.yml, i el servidor només necessita tenir Docker instal·lat. Això redueix el paper d’Ansible a la preparació del host. - Imatges immutables: es construeix una imatge de servidor (amb Packer o eines similars) que ja inclou tot el programari i la configuració. Cada canvi genera una imatge nova. Ansible no intervé en producció — només en la construcció de la imatge.
A la pràctica, molts entorns combinen enfocaments: Ansible configura el servidor base (usuaris, firewall, Docker) i Docker gestiona les aplicacions. La secció de Patrons d’ús mostra exemples concrets d’aquestes combinacions.
Patrons d’ús
Les eines d’IaC es combinen de maneres diferents segons el context. Per situar cada patró, recordem la matriu de la introducció: aprovisionament i configuració operen a dos nivells (infraestructura i aplicació), i cada patró cobreix una combinació diferent d’aquests quatre quadrants.
A nivell d’aplicació, Docker Compose és l’eina dominant i apareix en gairebé tots els patrons. Però no és l’única opció: Ansible pot desplegar aplicacions directament — instal·lar el runtime (Node, Python…), copiar el codi, configurar el servei amb systemd — sense contenidors.
Quan l’aplicació creix més enllà d’un sol servidor, Kubernetes substitueix Compose en aquest nivell. Kubernetes orquestra contenidors a través de múltiples nodes i ofereix funcionalitats que Compose no pot donar: escalat automàtic (ajustar el nombre de rèpliques segons la càrrega), desplegaments sense downtime (rolling updates, canaris), auto-reparació (reiniciar contenidors que fallen, redistribuir càrrega si un node cau), i service discovery integrat entre serveis distribuïts. El preu és una complexitat considerable: Kubernetes requereix un clúster dedicat, un coneixement profund de la seva arquitectura (pods, services, deployments, ingress…) i un esforç operacional significatiu. Per a la majoria d’aplicacions — un servidor o uns pocs — Compose amb Ansible és suficient i molt més senzill. Kubernetes queda fora de l’abast d’aquest document.
Cada patró cobreix una combinació diferent d’aquests quatre quadrants. Els patrons següents van de menys a més complets.
Docker Compose com a IaC d’aplicació
Nivell: aplicació — aprovisionament (crea contenidors, xarxes, volums) i configuració (variables, ports, dependències). No cobreix el nivell d’infraestructura.
Si has treballat amb Docker Compose, ja has fet IaC sense saber-ho. Un fitxer compose.yml és una descripció declarativa de la infraestructura de l’aplicació: quins serveis s’executen, com es connecten entre ells, quines dades persisteixen.
Per als fonaments de Docker i Compose, consulteu Docker bàsic. Aquí ampliem el concepte cap a arquitectures multi-servei més realistes.
Un entorn de desenvolupament típic pot incloure quatre serveis: l’aplicació, una base de dades, un sistema de cache i un reverse proxy:
services:
app:
build: .
environment:
- DATABASE_URL=postgresql://db:5432/app
- REDIS_URL=redis://cache:6379
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
db:
image: postgres:16
volumes:
- db_data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=app
- POSTGRES_PASSWORD=${DB_PASSWORD}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 5
cache:
image: redis:7-alpine
proxy:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf
depends_on:
- app
volumes:
db_data:
Aquest fitxer defineix tota la topologia de l’aplicació: serveis, xarxes, volums, dependències i health checks. Està versionat al repositori, qualsevol membre de l’equip pot aixecar l’entorn amb un sol docker compose up, i el resultat és idèntic a tot arreu.
Compose fa d’eina d’IaC al nivell de l’aplicació: aprovisiona (crea contenidors, xarxes, volums) i configura (variables, ports, dependències) els serveis. Però opera dins d’una màquina que ja existeix i ja té Docker instal·lat. No crea la màquina virtual, no configura el firewall, no instal·la el sistema operatiu. Per al nivell d’infraestructura necessitem Terraform i Ansible.
Ansible + Docker
Nivell d’infraestructura: configuració (Ansible instal·la Docker, firewall, certificats). Nivell d’aplicació: aprovisionament i configuració (Compose desplega els serveis).
Un patró molt habitual en entorns de producció és combinar Ansible per configurar el servidor i Docker per desplegar l’aplicació. Ansible s’encarrega de tot el que el contenidor no pot fer per si sol: instal·lar Docker, configurar el firewall, gestionar certificats TLS, crear directoris de dades… I després desplega l’aplicació com a contenidors.
El flux típic és:
- Ansible instal·la Docker al servidor (mòdul
apto rolgeerlingguy.docker) - Ansible genera el
compose.ymla partir d’una template Jinja2, injectant les variables de l’entorn - Ansible executa
docker compose upal servidor remot
Exemple d’un playbook que desplega una aplicació amb Docker Compose:
---
- name: Desplegar aplicació
hosts: webservers
become: true
tasks:
- name: Instal·lar Docker
apt:
name:
- docker.io
- docker-compose-v2
state: present
update_cache: true
- name: Crear directori de l'aplicació
file:
path: /opt/app
state: directory
- name: Generar docker-compose.yml
template:
src: docker-compose.yml.j2
dest: /opt/app/docker-compose.yml
notify: Reiniciar aplicació
- name: Generar fitxer d'entorn
template:
src: env.j2
dest: /opt/app/.env
mode: '0600'
notify: Reiniciar aplicació
handlers:
- name: Reiniciar aplicació
command: docker compose up -d
args:
chdir: /opt/app
La template docker-compose.yml.j2 pot ser:
services:
app:
image: {{ app_image }}:{{ app_version }}
env_file: .env
ports:
- "{{ app_port }}:8080"
restart: unless-stopped
db:
image: postgres:16
volumes:
- db_data:/var/lib/postgresql/data
env_file: .env
restart: unless-stopped
volumes:
db_data:
Ansible també pot interactuar amb Docker directament mitjançant els mòduls de la col·lecció community.docker: docker_image per gestionar imatges, docker_container per a contenidors individuals, o docker_compose_v2 per a Compose.
L’avantatge d’aquesta combinació és doble:
- Docker proporciona portabilitat i aïllament de l’aplicació
- Ansible proporciona gestió de configuració, reproductibilitat i automatització del servidor sencer
Terraform + Ansible + Docker
Nivell d’infraestructura: aprovisionament (Terraform crea VMs, xarxes) i configuració (Ansible instal·la paquets, configura serveis). Nivell d’aplicació: aprovisionament i configuració (Compose desplega els serveis).
És el patró més complet: cobreix els quatre quadrants de la matriu. Cada eina fa el que millor sap fer:
- Terraform aprovisiona la infraestructura: crea les VMs, xarxes i recursos necessaris
- Ansible configura els servidors: instal·la Docker, configura el firewall, gestiona certificats, crea usuaris
- Docker Compose desplega l’aplicació: defineix els serveis, xarxes i volums de l’aplicació
El flux és el que hem vist a la introducció: Terraform crea les màquines i n’exporta les IPs, Ansible s’hi connecta per configurar-les, i finalment hi desplega l’aplicació amb Compose. A la pràctica, els passos 2 i 3 sovint s’executen amb un sol ansible-playbook que inclou tant la configuració del servidor com el desplegament amb Compose.
Aquest patró és habitual en entorns amb infraestructura gestionada (Proxmox, OpenStack, núvol públic) on cal crear i destruir recursos amb freqüència.
Terraform + cloud-init
Nivell d’infraestructura: aprovisionament (Terraform crea VMs) i configuració mínima (cloud-init al primer boot). Nivell d’aplicació: aprovisionament i configuració (Compose desplega els serveis).
En aquest patró, Ansible desapareix del flux. La configuració del servidor es fa amb cloud-init: un estàndard que permet executar scripts i configuració al primer arrencada d’una VM. Terraform passa les instruccions de cloud-init com a part de la definició del recurs:
resource "proxmox_virtual_environment_vm" "web" {
name = "web-server"
# ...
initialization {
user_data_file_id = proxmox_virtual_environment_file.cloud_init.id
}
}
resource "proxmox_virtual_environment_file" "cloud_init" {
content_type = "snippets"
datastore_id = "local"
node_name = "pve1"
source_raw {
data = <<-EOF
#cloud-config
packages:
- docker.io
- docker-compose-v2
runcmd:
- systemctl enable docker
- cd /opt/app && docker compose up -d
EOF
file_name = "cloud-init.yml"
}
}
La VM arrenca ja amb Docker instal·lat i l’aplicació en marxa. No cal una connexió SSH posterior ni un pas addicional de configuració.
L’avantatge és la simplicitat: tot queda definit a Terraform, sense una segona eina. El compromís és que cloud-init és menys potent que Ansible — funciona bé per a configuracions senzilles, però es queda curt per a escenaris complexos (múltiples rols, templates, gestió de secrets, configuració condicional). S’apropa al model d’infraestructura immutable: si cal canviar la configuració, es recrea la VM en lloc de modificar-la.
Ansible com a aprovisionador
Nivell d’infraestructura: aprovisionament (Ansible crea VMs, contenidors LXC) i configuració (Ansible instal·la paquets, configura serveis). No cobreix el nivell d’aplicació per si sol, però es pot combinar amb Docker Compose.
Com hem vist a la secció de Terraform, Ansible també pot fer d’aprovisionador: crear recursos directament amb els seus mòduls, sense necessitat d’una eina addicional. Això és especialment útil en entorns autogestionats on Ansible ja gestiona la configuració.
Per exemple, amb contenidors LXC (Linux Containers) — que són com VMs lleugeres amb sistema operatiu complet, init propi i xarxa, però compartint el kernel del host — Ansible pot crear-los i configurar-los en un sol flux. El mateix patró s’aplica a VMs de Proxmox (amb el mòdul community.general.proxmox_kvm) o instàncies d’OpenStack (amb openstack.cloud.server).
El flux és sempre el mateix:
- Ansible es connecta a la plataforma (host LXC, API de Proxmox, API d’OpenStack)
- Ansible crea els recursos (contenidors, VMs, instàncies) segons les definicions
- Ansible configura cada recurs (paquets, serveis, aplicacions) connectant-s’hi per SSH
Exemple amb contenidors LXC — l’inventari defineix tant els contenidors a crear com la seva configuració:
all:
children:
lxc_host:
hosts:
server1:
ansible_host: 192.168.1.5
containers:
- name: web
template: ubuntu-22.04
ip: 10.0.3.10
role: webserver
- name: db
template: ubuntu-22.04
ip: 10.0.3.20
role: database
Un playbook simplificat per crear i configurar els contenidors:
---
- name: Crear contenidors LXC
hosts: lxc_host
become: true
tasks:
- name: Crear contenidor
community.general.lxc_container:
name: "{{ item.name }}"
template: "{{ item.template }}"
state: started
container_config:
- "lxc.net.0.ipv4.address = {{ item.ip }}/24"
loop: "{{ containers }}"
- name: Configurar servidors web
hosts: webservers
become: true
roles:
- common
- nginx
El compromís d’aquest enfocament és el que hem vist: es perd el fitxer d’estat, el cicle plan/apply i la gestió de dependències que ofereix Terraform. A canvi, es guanya simplicitat — una sola eina per a tot.
Comparació dels patrons:
| Patró | Aprovis. infra | Config. infra | Aprovis. + config. app | Cas d’ús típic |
|---|---|---|---|---|
| Compose | — | — | Compose | Desenvolupament local, entorns simples |
| Ansible + Docker | — | Ansible | Compose | Producció amb servidor existent |
| Terraform + Ansible + Docker | Terraform | Ansible | Compose | Entorns al núvol o Proxmox (flux complet) |
| Terraform + cloud-init | Terraform | cloud-init | Compose | Infraestructura immutable, config. senzilla |
| Ansible aprovisionador | Ansible | Ansible | — (o Compose) | Entorns autogestionats, simplicitat |
Tots els patrons comparteixen els mateixos principis d’IaC: tot definit en fitxers, versionat a Git, i aplicable automàticament.
Gestió de secrets
Els fitxers d’IaC viuen al repositori de Git, però necessiten accés a contrasenyes, claus d’API i certificats. Aquests secrets no poden estar al repositori en text pla — tal com s’explica a DevOps.
Hi ha tres nivells de gestió de secrets, de menys a més sofisticat:
Ansible Vault
Ansible Vault permet encriptar fitxers o variables individuals amb una contrasenya. El fitxer encriptat es pot versionar a Git amb seguretat:
# Encriptar un fitxer de variables
ansible-vault encrypt group_vars/production/secrets.yml
# Editar secrets sense desencriptar manualment
ansible-vault edit group_vars/production/secrets.yml
# Executar un playbook amb secrets encriptats
ansible-playbook playbook.yml --ask-vault-pass
El fitxer secrets.yml abans d’encriptar:
db_password: "s3cur3_p4ssw0rd"
api_key: "ak_live_xxxxxxxxxxxx"
tls_certificate: |
-----BEGIN CERTIFICATE-----
...
Un cop encriptat, Git veu un fitxer binari que només es pot desxifrar amb la contrasenya del vault. Ansible el desencripta automàticament en temps d’execució i injecta les variables a les tasques i templates.
El flux pràctic és:
- El fitxer encriptat viu al repositori (segur)
- El desenvolupador o el pipeline de CI/CD té la contrasenya del vault
- Ansible desencripta en runtime → injecta valors a templates/variables
- Mai es guarden secrets en text pla al disc del servidor
Variables de CI/CD
Les plataformes de CI/CD (GitLab CI, GitHub Actions) ofereixen magatzems de secrets integrats. Els secrets es configuren a la interfície de la plataforma i s’injecten com a variables d’entorn durant l’execució del pipeline:
# .gitlab-ci.yml
deploy:
script:
- ansible-playbook playbook.yml --extra-vars "db_password=$DB_PASSWORD"
variables:
ANSIBLE_VAULT_PASSWORD: $VAULT_PASSWORD
Això permet separar els secrets del codi completament: el repositori no conté cap secret, ni tan sols encriptat.
Gestors externs
Per a entorns més complexos, existeixen gestors de secrets dedicats com HashiCorp Vault. Ansible pot consultar-los directament amb plugins de lookup. Això és habitual en organitzacions grans, però queda fora de l’abast d’aquesta introducció.
Integració amb CI/CD
IaC tanca el cercle del pipeline de DevOps: els fitxers d’infraestructura viuen al repositori, es revisen amb pull requests i s’apliquen automàticament.
El pipeline d’infraestructura segueix el mateix patró que el de codi:
git push → validar → dry-run → [aprovació] → aplicar
En detall:
- Validar: comprovar la sintaxi (
ansible-lint,terraform validate) - Dry-run: simular l’execució sense aplicar canvis (
terraform plan,ansible-playbook --check) - Aprovació manual: per a producció, una persona revisa els canvis proposats
- Aplicar: executar els canvis (
terraform apply,ansible-playbook)
El mecanisme que dispara aquest pipeline és un hook de Git, tal com s’explica a DevOps. Quan algú fa git push, un hook post-receive al servidor executa els passos del pipeline. Això funciona amb qualsevol servidor Git — des d’un repositori autogestionat amb Gitolite fins a plataformes com GitLab.
Amb hooks de Git
En un entorn autogestionat (per exemple, Gitolite), el hook post-receive pot executar directament Ansible:
#!/bin/bash
# hooks/post-receive
while read oldrev newrev ref; do
if [ "$ref" = "refs/heads/main" ]; then
echo "Desplegant infraestructura..."
ansible-lint playbook.yml \
&& ansible-playbook -i inventory/staging.yml playbook.yml
fi
done
Això és tot el que cal per tenir un pipeline bàsic: un git push a main valida i desplega automàticament. Per a producció, es pot afegir un pas manual (per exemple, un push a una branca production o una etiqueta).
Amb plataformes de CI/CD
Plataformes com GitLab CI o Jenkins afegeixen una capa d’abstracció sobre els hooks: en lloc d’escriure scripts bash, es defineix el pipeline en un fitxer YAML amb etapes, condicions i aprovacions manuals. L’avantatge respecte als hooks manuals és la interfície visual, la gestió de secrets integrada i les eines de monitorització. Però el concepte subjacent és el mateix: un event de Git que dispara una seqüència automatitzada.
Referències
Programació en Java
- Java bàsic
- Recursivitat
- Ordenació
- Col.leccions i mapes
- Excepcions
- Interfícies gràfiques
- Persistència POO
- Píndoles Java
- Glossari Java
- Eines Java
Java bàsic
- Programes
- Execució
- Hola, món
- Estructura i sintaxi
- Tipus de dades
- Cadenes
- Aritmètica i condicions
- Conversions
- Variables
- Mètodes
- Control de flux
- Classes i objectes
- Interfícies
- Arrays
- Col·leccions
- Definició de classes
- Igualtat
- Definició d’interfícies
- Classes internes
- Definició d’iteradors
- Herència
- Classes abstractes
- Gestió d’errors
- Referències
Programes
Els programes consten d’interfícies i classes. Les interfícies i les classes es troben en fitxers font (extensió .java). Un fitxer font es compila en un o més fitxers de bytecode executables (extensió .class).
Les classes i les interfícies poden formar part d’un package. Un package és una mica com un mòdul, des del qual es poden importar recursos. Els fitxers de bytecode d’un package solen estar continguts en un directori el nom del qual és el nom del package.
Execució
Els programes primer s’han de compilar abans de poder ser executats. Un cop compilats, es poden executar en un ordinador. Per poder executar un programa Java compilat cal un intèrpret de bytecode anomenat màquina virtual Java (JVM).
Primer s’ha de compilar una aplicació Java, de la manera següent:
javac HelloWorld.java
El fitxer de bytecodes es pot executar de la manera següent:
java HelloWorld
Els errors de sintaxi i tipus es detecten en temps de compilació. Tots els altres errors es detecten en temps d’execució.
Hola, món
La versió més senzilla d’aquest programa defineix un mètode main dins d’una classe:
public class HelloWorld {
static public void main(String[] args) {
System.out.println("Hola món!");
}
}
El mètode println, quan s’executa amb la variable de classe System.out, converteix les dades en text, les mostra i mou el cursor a la línia següent. Si es vol evitar la sortida d’un salt de línia, utilitzeu el mètode print.
El codi font defineix però no crida aquest mètode. A l’inici del programa, la classe compilada HelloWorld es carrega a la JVM. La JVM crida llavors main, que és un mètode de classe a HelloWorld.
Una aplicació Java ha d’incloure almenys una classe que defineixi un mètode main. El fitxer de codi de bytes per a aquesta classe és el punt d’entrada per a l’execució del programa.
Estructura i sintaxi
-
Els literals inclouen números, caràcters i cadenes.
-
Els identificadors inclouen els noms de variables, classes, interfícies i mètodes.
-
Les paraules reservades inclouen les de les principals sentències de control (
if,while,for,import, etc.), operadors (instanceof,throw, etc.), definicions (public, class, etc.), valors especials (true,false,null,this,super, etc.) i noms de tipus estàndard (int,double,String, etc.). -
La sagnia no és significativa, de manera que tots els elements lèxics estan separats per zero o més espais.
-
Els blocs de codi en les sentències i definicions estan entre claus ({}).
-
Les sentències simples acaben amb un punt i coma (;).
-
Les expressions booleanes en els bucles i les sentències if estan entre parèntesis.
-
Un comentari de final de línia comença amb el símbol //.
// Això és un comentari de final de // línia // (en tres línies).Un comentari de diverses línies comença amb /i acaba amb/.
/* Aquest és un comentari de diverses línies. */
Tipus de dades
Hi ha dues grans categories de tipus de dades: els tipus primitius i els tipus de referència.
- Els tipus primitius inclouen els tipus numèrics (double, int, char) i els booleans. Els valors dels tipus primitius són immutables i no són instàncies de classes.
- “int” representa nombres enters que van des de -231 fins a 231-1 (4 bytes).
- “long” representa nombres enters més grans que van des de -263 fins a 263-1 (8 bytes).
- “short” representa nombres enters més petits que van des de -215 fins a 215-1 (2 bytes).
- “float” representa nombres de coma flotant més petits amb 7 dígits de precisió (4 bytes).
- “double” representa nombres de coma flotant més grans amb 16 dígits de precisió (8 bytes).
- Els tipus de referència són classes. Per tant, qualsevol objecte o instància d’una classe és d’un tipus de referència. Aquests inclouen cadenes, matrius, llistes, mapes, etc.
Cadenes
- Els literals de cadena es formen utilitzant les cometes dobles com a delimitadors.
- Les cadenes són instàncies de la classe String. Les cadenes són objectes immutables.
- Els literals de caràcters es formen utilitzant les cometes simples com a delimitadors.
- Els caràcters són valors del tipus de dades char. Aquest tipus utilitza 2 bytes per representar el conjunt Unicode.
- Una seqüència d’escapada, ja sigui com a caràcter o com a cadena, es forma utilitzant el caràcter ’' seguit d’una lletra adequada com ara ‘n’ o ‘t’.
L’operador de concatenació + uneix dues cadenes per formar una tercera cadena nova. Si un dels operands és una cadena, l’altre operand pot ser de qualsevol tipus.
Exemples:
"35" + " pàgines de llargada."
35 + " pàgines de llargada."
Qualsevol objecte no numèric també es pot utilitzar en una concatenació de cadenes, perquè tots els objectes Java reconeixen el mètode toString(). Aquest mètode retorna el nom de la classe de l’objecte per defecte, però es pot substituir per retornar una cadena més descriptiva. Així, si x i y són objectes, el codi
x.toString() + y.toString()
// o bé
x + y
concatena les seves representacions de cadenes.
Una cadena és una seqüència de 0 o més caràcters.
Les cadenes són instàncies de la classe String. Les cadenes són objectes immutables.
El mètode length() retorna el nombre de caràcters d’una cadena.
El mètode charAt accedeix a un caràcter en una posició determinada.
Exemple:
String aString = "Hola món!"
System.out.println(aString.length());
System.out.println(aString.charAt(2));
La classe String inclou molts mètodes útils per cercar, obtenir subcadenes, etc.
Les cadenes s’han de comparar utilitzant els mètodes equals i compareTo.
Aritmètica i condicions
Els operadors aritmètics inclouen +, -, *, / i %.
Dos operands sencers donen un resultat sencers. Com a mínim un operand float dóna un resultat float.
La classe Math inclou mètodes de classe com ara round, max, min, abs i pow (exponenciació), així com mètodes per a trigonometria, logaritmes, arrels quadrades, etc.
Exemples:
Math.round(3.14)
Math.sqrt(2)
Els operadors de comparació són ==, !=, <, >, <= i >=.
Tots els operadors de comparació retornen True o False.
Tots els valors dels tipus primitius són comparables.
Els valors dels tipus de referència són comparables si i només si reconeixen el mètode compareTo. compareTo retorna 0 si els dos objectes són iguals (utilitzant el mètode equals), un enter negatiu si l’objecte receptor és menor que l’objecte paràmetre i un enter positiu si l’objecte receptor és major que l’objecte paràmetre. Les classes d’objectes comparables generalment implementen la interfície Comparable, que inclou el mètode compareTo.
Exemple:
String s = "AAB";
System.out.println(s.compareTo("AAA") > 0);
El tipus booleà inclou els valors constants true i false.
Els operadors lògics són ! (not), && (and) i || (or).
Les expressions booleanes compostes consten d’un o més operands booleans i un operador lògic. L’avaluació en curtcircuit s’atura quan hi ha prou informació disponible per retornar un valor. ! s’avalua abans de &&, que s’avalua abans de ||.
Conversions
Els tipus numèrics es poden convertir en altres tipus numèrics mitjançant els operadors de conversió adequats. Un operador de conversió es forma tancant el nom del tipus de destinació entre parèntesis.
Exemples:
(int) 3.14
(double) 3
(char) 45
(int) 'a'
Quan es converteix un nombre enter en un caràcter o viceversa, se suposa que el nombre enter és el valor Unicode del caràcter.
La manera més senzilla de convertir qualsevol valor en una cadena és concatenar-lo amb una cadena buida, de la manera següent:
"" + 3.14
"" + 45
Variables
<tipus> <variable>, …, <variable>;
<tipus> <variable> = <expressió>;
Exemples:
int x, y;
x = 1;
y = 2;
int z = 3;
Una variable té un tipus, que s’especifica quan es declara. A una variable només se li pot assignar un valor que sigui compatible amb el seu tipus. Les incompatibilitats de tipus es detecten en temps de compilació.
A totes les variables d’instància i de classe se’ls donen valors per defecte quan es declaren. Tanmateix, el compilador requereix que el programador assigni un valor a les variables temporals dins dels mètodes abans que es puguin fer referència a elles. Per tant, es garanteix que totes les variables tinguin un valor en temps d’execució.
Les assignacions tenen la forma:
Forma:
<variable> = <expressió>;
Exemple:
int x;
x = 1;
x += 1;
Una variable té un tipus, que s’especifica quan es declara. A una variable només se li pot assignar un valor que sigui compatible amb el seu tipus.
Els valors de tipus menys inclusius es poden assignar a variables de tipus més inclusius. Invertir aquest ordre requereix una conversió de tipus explícita abans de l’assignació.
Exemple:
double d;
d = 34;
int i;
i = (int)3.14
Mètodes
Una crida a un mètode d’instància normalment consisteix en una referència d’objecte (també anomenada objecte receptor), seguida d’un punt, seguit del nom del mètode i una llista d’arguments entre parèntesis.
Exemple:
aList.set(3, 45);
Una crida a un mètode de classe consisteix en un nom de classe, seguit d’un punt, seguit del nom del mètode i una llista d’arguments entre parèntesis.
Exemple:
double d = Math.sqrt(2);
L’instrucció return surt d’un mètode. Si no s’especifica cap expressió, es retorna el valor void. El valor retornat ha de ser de tipus compatible amb el tipus de retorn del mètode.
return 0;
Tots els mètodes estan definits per retornar un tipus de valor específic. Quan no s’espera el valor de retorn, el tipus de retorn del mètode és void. En cas contrari, el tipus de valor retornat ha de ser compatible, en temps de compilació, amb el tipus de valor esperat (l’exemple mostra una assignació d’un double a un double).
Un recurs de paquet s’importa mitjançant la forma:
import <nom del paquet>.<nom del recurs>;
A continuació, es fa referència al recurs sense el nom del package com a qualificador.
Exemple:
import javax.swing.JButton;
JButton b = new JButton("Restablir");
Alternativament, es poden importar tots els recursos del paquet mitjançant el formulari
import <nom del paquet>.*;
De vegades, dos recursos tindran el mateix nom en paquets diferents. Per utilitzar tots dos recursos, no els importeu, sinó que només feu-hi referència utilitzant els noms dels paquets com a qualificadors.
Exemple:
java.util.List<String> names = new java.util.ArrayList<String>();
java.awt.List namesView = new java.awt.List();
Control de flux
if
Forma:
if (<expressió booleana>) {
// instruccions
} else if (<expressió booleana>) {
// instruccions
} else {
// instruccions
}
Les instruccions del conseqüent i de cada alternativa estan marcades amb claus ({}). Quan només hi ha una instrucció, es poden ometre les claus.
Cada expressió booleana està entre parèntesis.
while
Els bucles poden fer-se amb while:
while (<expressió booleana>) {
// instruccions
}
Les instruccions del cos del bucle estan marcades amb claus ({}). Quan només hi ha una instrucció al cos del bucle, es poden ometre les claus.
L’expressió booleana s’inclou entre parèntesis.
L’instrucció break surt d’un bucle.
while (true)
break;
for
Hi ha dos tipus, un bucle per visitar cada element d’un objecte iterable i un bucle amb el mateix comportament que un bucle while.
Forma del primer tipus (també anomenada bucle for millorat):
for (<tipus> <variable>: <iterable>) {
// instruccions
}
Exemple:
for (String s: aListOfStrings) {
System.out.println(s);
}
La variable recull el valor de cada element de l’objecte iterable i és visible al cos del bucle.
Les instruccions del cos del bucle estan marcades amb claus ({}). Quan només hi ha una instrucció al cos del bucle, es poden ometre les claus.
Forma del segon tipus:
for (<inicialitzador>; <continuació>; <actualització>) {
// instruccions
}
Exemple:
for (int i = 1; i <= 10; i++) {
System.out.println(i);
}
Aquest bucle té el mateix comportament que el següent bucle while:
int i = 1;
while (i <= 10) {
System.out.println(i);
i++;
}
Classes i objectes
La forma per a la instanciació d’objectes és
new <nom de la classe>(<arguments>)
Exemple:
CompteCorrent compte = new CompteCorrent("Ken", "1111", 500.00);
Les col·leccions genèriques també requereixen un o més paràmetres de tipus per als tipus d’elements, de la forma
new <nom de la classe><tipus d'elements>(<arguments>)
Exemple:
List<String> noms = new ArrayList<String>();
Les variables i els paràmetres es declaren amb un tipus. A una variable o paràmetre només se li pot assignar un valor que sigui compatible amb el seu tipus.
Els valors de tipus menys inclusius es poden assignar a variables o paràmetres de tipus més inclusius. Invertir aquest ordre requereix una conversió de tipus explícita abans de l’assignació.
Exemple:
String s1 = "Hello";
Object obj = s1;
String s2 = (String)obj;
void aMethod(String s) {
String other = s;
return;
}
aMethod(s1);
aMethod((String)obj);
Interfícies
Una interfície consta d’un nom i un conjunt de capçaleres de mètode. Especifica el conjunt de mètodes que una classe d’implementació ha d’incloure. Les interfícies de les classes integrades es poden veure al JavaDoc.
Una sola classe pot implementar diverses interfícies diferents.
Una interfície garanteix un comportament abstracte comú de totes les classes d’implementació. Per exemple, la interfície java.util.List especifica mètodes per a totes les classes de llistes, incloent-hi java.util.ArrayList i java.util.LinkedList.
Una interfície pot estendre una altra interfície més general. Per exemple, la interfície List estén la interfície Collection. Això significa que tots els mètodes requerits per la interfície Collection també seran necessaris per a totes les llistes.
Les interfícies són com la cola intersticial que uneix els components del programa. Sempre que sigui possible, utilitzeu noms d’interfície per als tipus de variables, paràmetres i tipus de retorn de mètodes.
Usos de les interfícies:
List<String> aList = new ArrayList<String>();
aList.add(("Mary");
aList.add("Bill");
Collections.sort(aList);
La classe ArrayList implementa la interfície List. Observeu l’ús de List en lloc d’ArrayList i LinkedList per escriure les variables list1 i list2.
El mètode Collections.sort espera una col·lecció de comparables com a argument. Com que la classe String implementa la interfície Comparable i la interfície List estén la interfície Collection, el compilador no es queixa i tot va bé.
Finalment, observeu que el constructor LinkedList pot acceptar una col·lecció com a argument. Això permet construir una nova llista a partir dels elements continguts en una altra llista, independentment de la implementació.
Arrays
Una matriu és una seqüència d’elements del mateix tipus. S’accedeix a cada element en temps constant mitjançant una posició d’índex. A diferència d’una llista, la longitud d’una matriu és fixa quan es crea i no es pot canviar. A més, les úniques operacions en una matriu són l’accés o la substitució d’elements mitjançant subíndexs.
Com altres estructures de dades, les matrius són objectes i, per tant, són de tipus referència. El tipus d’una matriu està determinat pel seu tipus d’element, que s’especifica quan s’instancia la matriu.
Exemples:
int[] ages = new int[10];
String[] names = new String[10];
Les variables ages i names ara fan referència a matrius capaces de contenir 10 sencers i 10 cadenes, respectivament. Cada element de la matriu a ages té un valor per defecte de 0. Cada element de la matriu a names té un valor per defecte de null (igual que una nova matriu els elements de la qual són de qualsevol tipus de referència).
La longitud d’una matriu es pot obtenir de la variable de longitud de l’objecte matriu, de la següent manera:
System.out.println(names.length);
Els elements de la matriu names es poden reiniciar amb un bucle for, utilitzant el subíndex i la variable length, de la manera següent:
java.util.Scanner input = new java.util.Scanner(System.in);
for (int i = 0; i < names.length; i++)
names[i] = reader.nextLine("Introduïu un nom: ");
El bucle for millorat es pot utilitzar només per fer referència als elements d’una matriu:
for (String name: names)
System.out.println(name);
Col·leccions
| Interfície | Implementació comuna | Ordenat? | Permet duplicats? |
|---|---|---|---|
List<E> | ArrayList, LinkedList | Sí (ordre d’inserció) | Sí |
Set<E> | HashSet | No | No |
Set<E> | LinkedHashSet | Sí (ordre d’inserció) | No |
Set<E> | TreeSet | Sí (ordre natural o Comparator) | No |
Map<K,V> | HashMap | No | Claus úniques |
Map<K,V> | LinkedHashMap | Sí (ordre d’inserció) | Claus úniques |
Map<K,V> | TreeMap | Sí (ordre natural o Comparator) | Claus úniques |
Queue<E> | LinkedList, ArrayDeque | Sí (FIFO) | Sí |
Deque<E> | ArrayDeque | Sí (doble FIFO) | Sí |
Stack<E> | Stack | Sí (LIFO) | Sí |
Totes les col·leccions de la taula són mutables.
Llistes
Una llista és una seqüència mutable de 0 o més objectes de qualsevol tipus. Una llista genèrica restringeix els seus elements al mateix supertipus.
La interfície List inclou els mètodes comuns a totes les classes d’implementació.
Les classes d’implementació inclouen ArrayList i LinkedList.
Una llista genèrica especifica el tipus d’element de la variable de llista i l’objecte de llista instanciat, de la manera següent:
List<String> names = new ArrayList<String>();
List<Integer> ages = new LinkedList<Integer>();
Tingueu en compte primer que ambdues variables de llista es veuen com a de tipus List, tot i que fan referència a instàncies de classes de llista diferents. Tots dos objectes de llista respondran a qualsevol mètode de la interfície List.
Tingueu en compte en segon lloc que la primera llista només pot contenir cadenes, mentre que la segona llista només pot contenir instàncies de la classe Integer.
Tingueu en compte en tercer lloc que la classe Integer és una classe contenidora, que permet emmagatzemar valors de tipus int en una llista. Quan s’insereix un enter a la segona llista, la JVM l’embolica en un objecte Integer. Quan s’accedeix a un objecte Integer en aquesta llista, es retorna el valor int que conté.
Exemple:
ages.add(63);
ages.set(0, ages.get(0) + 1); // Incrementa l'edat
Sets
Un conjunt és una col·lecció mutable de 0 o més objectes únics de qualsevol tipus. Un conjunt genèric restringeix els seus elements al mateix supertipus.
La interfície Set inclou els mètodes comuns a totes les classes d’implementació. La interfície SortedSet estén la interfície Set per incloure mètodes per a conjunts ordenats.
Les classes d’implementació inclouen HashSet i TreeSet. Un TreeSet pot retornar els seus elements en ordre ordenat. Per tant, també implementa la interfície SortedSet.
Un conjunt genèric especifica el tipus d’element de la variable de conjunt i l’objecte de conjunt instanciat, de la manera següent:
Set<String> names = new HashSet<String>();
SortedSet<Integer> ints = new TreeSet<Integer>();
Exemple:
// Suma 10 enters aleatoris entre 1 i 10
for (int i = 1; i <= 10; i++)
ints.add((int)(Math.random() * 10 + 1));
// Imprimeix tots els elements ordenats
for (int element : ints)
System.out.println(element);
Observeu l’ús d’un bucle for per accedir als elements d’un conjunt.
També es pot crear un conjunt a partir d’una llista, de la manera següent:
Set<String> names = new HashSet<String>(listOfNames);
Les col·leccions Java solen incloure un constructor que accepta una altra col·lecció com a argument i afegeix els seus elements a la col·lecció recentment instanciada.
Diccionaris o mapes
Un mapa és una col·lecció mutable de 0 o més parells clau/valor únics. Dit d’una altra manera, un mapa conté un conjunt de claus, on cada clau està associada a un valor. Un mapa genèric restringeix les seves claus al mateix supertipus i els valors al mateix supertipus.
La interfície Map inclou els mètodes comuns a totes les classes d’implementació. La interfície SortedMap estén la interfície Map per incloure mètodes per a mapes ordenats.
Les classes d’implementació inclouen HashMap i TreeMap. Un TreeMap pot retornar les seves claus en ordre ordenat. Per tant, també implementa la interfície SortedMap.
Un mapa genèric especifica els tipus clau/valor de la variable de mapa i l’objecte de mapa instanciat, de la manera següent:
Map<String, Integer> names = new HashMap<String, Integer>();
SortedMap<Integer, Integer> ints = new TreeMap<Integer, Integer>();
Exemple:
// Associa 10 edats aleatòries entre 1 i 10
// amb noms consecutius
for (int i = 1; i <= 10; i++) {
String name = "Name" + i;
names.put(name, (int)(Math.random() * 10 + 1));
}
// Imprimeix totes les claus i els seus valors
for (String key : names.keySet())
System.out.println(key + " " + names.get(key));
Observeu l’ús d’un bucle for per accedir al conjunt de claus retornades per keySet().
Iterators
Un iterador és un objecte que admet el recorregut d’una col·lecció. El compilador genera codi que utilitza un iterador sempre que veu un bucle for millorat.
Totes les col·leccions implementen la interfície Iterable. Aquesta interfície inclou un únic mètode, iterator(), que retorna un objecte iterador.
Un objecte iterador implementa la interfície Iterator. Aquesta interfície inclou els mètodes next(), hasNext() i remove().
Com les col·leccions, els iteradors poden ser genèrics. Per tant, s’ha d’especificar el tipus d’element de la col·lecció genèrica quan es declara una variable de tipus Iterator.
Exemple d’ús:
// Crea una llista de cadenes
List<String> list = new ArrayList<String>();
// Afegeix algunes cadenes a list
// Obre un iterador a list
Iterator<String> i = list.iterator();
// Imprimeix totes les cadenes de text de list utilitzant l'iterador
while (i.hasNext()) {
String s = i.next();
System.out.println(s);
}
Definició de classes
Les definicions de classe tenen la forma general:
<modificador de visibilitat> class <nom>
extends <nom de la superclasse> implements <llista de noms> {
<variables de classe>
<mètodes de classe>
<variables d'instància>
<mètodes d'instància>
<classes internes>
}
Les classes que no estenen explícitament una altra classe estenen la classe Object per defecte. Una classe pot implementar zero o més interfícies.
Exemple:
public class Student {
public static final int NUM_GRADES = 5;
private String name;
private int[] grades;
public Student(String name) {
this.name = name;
this.grades = new int[NUM_GRADES];
}
public String getName() {
return this.name;
}
public int getGrade(int i) {
return this.grades[i – 1];
}
public void setGrade(int i, int newGrade) {
this.grades[i - 1] = newGrade;
}
public String toString() {
String result = this.name + '\n';
for (String grade : this.grades)
result += grade + ' ';
return result;
}
}
Ús:
Student s = new Student("Maria");
for (int i = 1; i <= Student.NUM_GRADES; i++)
s.setGrade(i, 100);
System.out.println(s);
Visibilitat
Hi ha quatre nivells d’accés a les classes i als elements definits dins d’elles. Hi ha tres modificadors de visibilitat que especifiquen l’accés: public, private i protected.
L’accés public permet que qualsevol component del programa faci referència a un element.
Exemple:
public static final int NUM_GRADES = 0;
L’accés privat permet l’accés a un element només per part d’altres elements dins de la definició de la classe que l’envolta.
Exemple:
private int[] grades;
L’accés protegit estén l’accés a un element des de la classe que el defineix a totes les subclasses.
Exemple:
protected String name;
Quan s’omet un modificador de visibilitat, l’element té accés al package. Això significa que l’accés s’estén des de la classe que el defineix a tots els components del mateix package. Per a la majoria de propòsits, l’accés al package és equivalent a l’accés public.
Variables i constructors
Les variables d’instància es declaren al mateix nivell que els mètodes dins d’una definició de classe. Normalment se’ls dóna accés privat per restringir la visibilitat. Poden rebre valors inicials quan es declaren o en un constructor.
Les referències a variables d’instància poden o no tenir el prefix reservat this.
A l’exemple següent, les variables this.name i this.grades són variables d’instància, mentre que la variable NUM_GRADES és una variable de classe:
public class Student {
public static final int NUM_GRADES = 5;
private String name;
private int[] grades;
public Student(String name) {
this.name = name;
this.grades = new int[NUM_GRADES];
}
}
La JVM crida automàticament el constructor quan el programador sol·licita una nova instància de la classe, de la següent manera:
Student s = new Student("Mary");
El constructor pot rebre un o més arguments per subministrar valors inicials per a les dades de l’objecte.
Un constructor per defecte no espera cap argument de la persona que truca i assigna valors per defecte raonables a les variables d’instància d’un objecte. Altres constructors esperen un o més arguments que permetin al programador especificar aquests valors.
La sobrecàrrega de mètodes permet definir més d’un constructor, sempre que tinguin nombres i/o tipus d’arguments diferents.
A l’exemple següent, la classe Student rep un constructor per defecte que no espera cap argument. El nou constructor crida l’altre constructor mitjançant la paraula clau this i l’argument apropiat:
public class Student {
public static final int NUM_GRADES = 5;
private String name;
private int[] grades;
public Student(String name) {
this.name = name;
this.grades = new int[NUM_GRADES];
}
public Student() {
this("");
}
}
Ús:
Student s1 = new Student("Mary");
Student s2 = new Student();
Mètodes d’instància
La forma de la definició d’un mètode d’instància és
<modificador de visibilitat> <tipus de retorn> <nom>(<arguments>) {
<sentències>
}
Tots els mètodes han d’especificar un tipus de retorn. Si no es retorna cap valor, aquest tipus ha de ser void. El valor retornat per qualsevol sentència de retorn ha de ser compatible amb el tipus de retorn del mètode. Un mètode no void ha de tenir almenys una sentència de retorn accessible.
Cada paràmetre de la capçalera del mètode, si n’hi ha, ha d’incloure el tipus del paràmetre. La sintaxi d’aquests és similar a la de les declaracions de variables.
Quan es crida un mètode, els seus arguments han de coincidir en nombre i tipus amb els paràmetres corresponents de la definició del mètode. Tota la comprovació de tipus es fa en temps de compilació.
Exemple:
public class Student {
// definició dels constructors i variables
public String getName() {
return this.name;
}
public int getGrade(int i) {
return this.grades[i – 1];
}
public void setGrade(int i, int novaGrade) {
this.grades[i - 1] = novaGrade;
}
}
La signatura d’un mètode consisteix en el seu nom i els tipus de paràmetre. El tipus de retorn no s’inclou a la signatura. Dos mètodes es sobrecarreguen si tenen el mateix nom però signatures diferents.
Per exemple, una classe Student pot tenir tres mètodes anomenats resetGrades per restablir totes les qualificacions. Tots són mètodes diferents.
El primer mètode, que no espera cap argument, restableix cada qualificació a 0:
public void resetGrades() {
resetGrades(0);
}
El segon mètode, que espera un únic argument sencer, restableix cada qualificació a aquest sencer.
public void resetGrades(int grade) {
for (int i = 0; i < Student.NUM_GRADES; i++)
this.grades[i] = grade;
}
El tercer mètode, que espera una matriu d’enters, restableix les qualificacions a aquests enters a les posicions corresponents de la matriu.
public void resetGrades(int[] notes) {
for (int i = 0; i < Student.NUM_GRADES; i++)
this.grades[i] = notes[i];
}
Ús:
Student s = new Student();
s.resetGrades(100);
s.resetGrades();
int[] newGrades = {85, 66, 90, 100, 73};
s.resetGrades(newGrades);
Mètodes i variables de classe
Les variables de classe anomenen dades que comparteixen totes les instàncies d’una classe.
Una declaració de variable de classe es qualifica amb la paraula reservada static.
Fora de la definició de la classe, les referències a variables de classe tenen com a prefix el nom de la classe.
Les variables de classe s’escriuen en majúscules per convenció.
Exemple:
public class Student {
public static final int NUM_GRADES = 5;
private String name;
private int[] grades;
public Student(String name) {
this.name = name;
this.grades = new int[NUM_GRADES];
}
}
Altres usos:
System.out.println(Student.NUM_GRADES);
Els mètodes de classe són mètodes que no saben res sobre les instàncies d’una classe, però poden accedir a variables de classe i cridar altres mètodes de classe per a diversos propòsits. Per exemple, un mètode per convertir una nota numèrica en una nota amb lletra es pot definir com un mètode de classe a la classe Student.
Una capçalera de mètode de classe es qualifica amb la paraula reservada static.
Fora de la definició de la classe, les crides a mètodes de classe tenen com a prefix el nom de la classe.
Exemple:
public class Student {
// Definicions de mètodes d'instància i declaracions de variables
public static char getLetterGrade(int grade) {
if (grade > 89)
return 'A';
else if (grade > 79)
return 'B';
else
return 'F';
}
}
Ús:
s = new Student();
for (int i = 1; i <= Student.NUM_GRADES; i++)
System.out.println(Student.getLetterGrade(s.getGrade(i));
Modificador final
Les variables finals serveixen com a constants simbòliques. Una declaració de variable final es qualifica amb la paraula reservada final. La variable s’estableix a un valor a la declaració i no es pot reiniciar. Qualsevol intent d’aquest tipus es detecta en temps de compilació.
Exemple:
public class Student {
public static final int NUM_GRADES = 5;
private String name;
private int[] notes;
public Student(String name) {
this.name = name;
this.grades = new int[NUM_GRADES];
}
}
Igualtat
El mètode equals() compara dos objectes per verificar la igualtat. Aquest mètode utilitza l’operador == per defecte. L’operador == comprova la igualtat de dues referències d’objectes: es refereixen exactament al mateix objecte a la memòria? Sovint, aquesta prova és massa restrictiva. Una versió més relaxada compararia un o més dels atributs dels objectes per verificar la igualtat. Per exemple, dos objectes Student podrien tenir el mateix nom i considerar-se iguals, tot i que també són objectes diferents. Aquest tipus d’igualtat s’anomena equivalència estructural, a diferència del tipus més restrictiu d’identitat d’objecte.
El programador pot anul·lar la definició per defecte d’equals incloent una definició d’aquest mètode en una classe determinada. Els atributs que es comparen són els de l’objecte receptor (this) i l’objecte paràmetre (other).
La capçalera del mètode per a equals és:
public boolean equals(Object other)
Tingueu en compte que el tipus de l’objecte paràmetre és Object. Això permet comparar qualsevol objecte amb l’objecte receptor per verificar la igualtat. En conseqüència, en realitat hi ha tres proves per realitzar en equals. La primera compara els dos objectes per verificar la identitat mitjançant ==. La segona utilitza l’operador instanceof per determinar si el tipus de l’objecte paràmetre és el mateix que el de l’objecte receptor. La tercera prova compara els atributs rellevants dels dos objectes utilitzant el mètode equals amb ells.
Abans d’accedir als atributs del paràmetre, el tipus del paràmetre s’ha de convertir al tipus de l’objecte receptor mitjançant un operador de conversió.
Exemple:
public class Student {
public static final int NUM_GRADES = 5;
private String name;
private int[] notes;
public Student(String name) {
this.name = name;
this.grades = new int[NUM_GRADES];
}
public boolean equals(Object other) {
if (this == other)
return true;
else if !(other instanceof Student)
return false;
else {
Student otherStudent = (Student)other;
return this.name.equals(otherStudent.name);
}
}
}
Ús:
Student s1 = new Student("Mary");
Student s2 = new Student("Bill");
Student s3 = new Student("Bill");
System.out.println(s1.equals(s2)); // mostra false
System.out.println(s2.equals(s3)); // mostra true
System.out.println(s1 == s3); // mostra false
System.out.println(! s2.equals(s3)); // mostra false
Comparable
Una classe d’objectes comparables implementa la interfície Comparable. Per exemple, la classe String implementa Comparable. Aquesta interfície especifica un únic mètode compareTo, de la següent manera:
public interface Comparable<E> {
public int compareTo(E element);
}
Aquesta interfície és genèrica; el tipus d’element E s’emplena amb la classe que l’implementa (normalment, el nom de la classe mateixa). El mètode compara els atributs rellevants de l’objecte receptor i l’objecte paràmetre. compareTo retorna 0 si els dos objectes són iguals (utilitzant el mètode equals), un enter menor que 0 si el receptor és menor que el paràmetre i un enter major que 0 en cas contrari. L’exemple següent utilitza els noms dels estudiants com a atributs comparables:
Exemple:
public class Student implements Comparable<Student> {
private String name;
public Student(String name) {
this.name = name;
}
public int compareTo(Student other) {
return this.name.compareTo(other.name);
}
}
Definició d’interfícies
La forma d’una interfície és
public interface <name> extends <name> {
<variables finals>
<capçaleres de mètode>
}
L’extensió d’una altra interfície és opcional. Una capçalera de mètode acaba amb un punt i coma.
En el següent exemple, la interfície TrueStack es defineix per a totes les implementacions que restringeixen les operacions a les estàndard de les piles. Com que TrueStack estén Iterable, cada implementació també ha d’incloure un mètode iterator(), i cada pila es pot recórrer amb un bucle for millorat.
El codi d’aquesta interfície es col·locaria al seu propi fitxer font, anomenat TrueStack.java. Aquest fitxer es pot compilar abans de definir cap classe d’implementació.
Exemple d’interfície:
public interface TrueStack<E> extends Iterable<E> {
public boolean isEmpty();
public E peek();
public E pop();
public void push(E element);
public int size();
}
Cada classe d’implementació es col·locaria al seu propi fitxer. Una interfície s’ha de compilar correctament abans de qualsevol de les seves classes d’implementació.
Exemple d’implementació, basat en una ArrayList:
import java.util.*;
public class ArrayStack<E> implements TrueStack<E> {
private List<E> list;
public ArrayStack() {
list = new ArrayList<E>();
}
public boolean empty() {
return list.isEmpty();
}
public E peek() {
return list.get(list.size() - 1);
}
public E pop() {
return list.remove(list.size() - 1);
}
public void push(E element) {
list.add(element);
}
public int size() {
return list.size();
}
public Iterator<E> iterator() {
return null; // TODO
}
}
Classes internes
Una classe es pot definir només per al seu ús en una altra definició de classe. Per exemple, una classe LinkedStack pot utilitzar una classe OneWayNode. La definició de OneWayNode es pot imbricar dins de LinkedStack. OneWayNode s’especifica com una classe privada, de vegades també anomenada classe interna. Aquí teniu una implementació:
public class LinkedStack<E> implements TrueStack<E>{
private OneWayNode<E> items;
private int size;
public LinkedStack() {
this.items = null;
this.size = 0;
}
public void push(E element) {
this.items = new OneWayNode<E>(element, items);
this.size += 1;
}
public E pop() {
E element = this.items.data;
this.items = this.items.next;
this.size -= 1;
return element;
}
public E peek() {
return this.items.data;
}
public boolean isEmpty() {
return this.size() == 0;
}
public int size() {
return this.size;
}
public Iterator<E> iterator() {
return null; // TODO
}
private class OneWayNode<E> {
private E data;
private OneWayNode next;
private OneWayNode(E data, OneWayNode next) {
this.data = data;
this.next = next;
}
}
}
Definició d’iteradors
El mètode iterator() retorna un iterador en un objecte iterable. L’usuari d’un iterador pot esperar que el mètode next() retorni el següent objecte en una iteració, mentre que el mètode hasNext() retorna True.
Suposem que la classe LinkedStack ara inclou un mètode iterador. Aleshores, es poden visitar els objectes d’una pila, de dalt a baix, de qualsevol de les maneres següents:
TrueStack<String> stack = new LinkedStack<String>();
i = stack.iterator();
while (i.hasNext()) {
String element = i.next()
// processament
}
for (String element: stack)
// processament
La classe d’implementació defineix un mètode iterator() que retorna una instància d’una classe interna. Aquesta classe implementa la interfície Iterator. Els seus mètodes next() i hasNext() rastregen un punter de posició actual als elements de la col·lecció.
Tingueu en compte que diversos iteradors poden estar oberts simultàniament a la mateixa col·lecció. Per mantenir la coherència de cada iterador amb les dades de la col·lecció, no es permeten modificacions basades en la col·lecció (push, pop) durant el funcionament de cap iterador. La col·lecció ara manté un recompte de les seves modificacions. Quan s’instancia un iterador, estableix el seu propi recompte de modificacions al recompte de la col·lecció. A cada crida del mètode next(), l’iterador compara els dos recomptes. Si no són iguals, s’ha produït una modificació basada en la col·lecció i es genera una excepció.
Exemple:
import java.util.iterator;
public class LinkedStack<E> implements TrueStack<E> {
private OneWayNode<E> items;
private int size;
private int modCount;
public LinkedStack() {
this.items = null;
this.size = 0;
this.modCount = 0;
}
public void push(E element) {
this.items = new OneWayNode<E>(element, items);
this.size += 1;
this.modCount += 1;
}
public E pop() {
E element = this.items.data;
this.items = this.items.next;
this.size -= 1;
this.modCount += 1;
return element;
}
public E peek() {
return this.items.data;
}
public boolean isEmpty() {
return this.size() == 0;
}
public int size() {
return this.size;
}
public Iterator<E> iterator() {
return new StackIterator<E>();
}
private class StackIterator<E> implements Iterator<E> {
private OneWayNode curPos;
private int curModCount;
private StackIterator() {
this.curPos = items;
this.curModCount = modCount;
}
public boolean hasNext() {
return curPos != null;
}
public E next() {
if (! this.hasNext()
throw new IllegalStateException();
if (this.curModCount != modCount)
throw new ConcurrentModificationException()
E data = this.curPos.data;
this.curPos = this.curPos.next();
return data;
}
public void remove() {
throw new UnsupportedOperationException();
}
}
private class OneWayNode<E> {
private E data;
private OneWayNode next;
private OneWayNode(E data, OneWayNode next) {
this.data = data;
This.next = next;
}
}
}
Herència
En un llenguatge de programació orientat a objectes, es pot definir una nova classe que reutilitza el codi d’una altra classe. La nova classe esdevé una subclasse de la classe existent, que també s’anomena la seva classe pare. La subclasse hereta tots els atributs, inclosos els mètodes i les variables, sempre que s’especifiquin com a públics o protegits, de la seva classe pare i de qualsevol classe ancestre de la jerarquia.
Fer subclasses és una manera convenient de proporcionar funcionalitats addicionals o més especialitzades a un recurs existent. Per exemple, una cua garanteix l’accés als seus elements en ordre de primer a entrar, primer a sortir. Una cua de prioritat es comporta en la majoria dels aspectes igual que una cua, excepte que els elements de més prioritat s’eliminen primer. Si els elements tenen la mateixa prioritat, s’eliminen en ordre FIFO estàndard. Una cua de prioritat és, doncs, una subclasse de cua, amb un mètode especialitzat per a insercions que garanteix l’ordre adequat dels elements. La cua de prioritat obté tots els seus altres mètodes de la classe de cua gratuïtament.
La classe LinkedQueue fa que les seves variables d’instància siguin visibles per a les subclasses però no per a les altres declarant-les com a protegides. De la mateixa manera, la classe OneWayNode interna i els seus atributs també es defineixen com a protegits.
Aquí teniu la definició d’una classe LinkedQueue per a cues ordinàries:
import java.util.*;
public class LinkedQueue<E> implements TrueQueue<E> {
protected OneWayNode<E> front, rear;
Protected int size;
public LinkedQueue() {
this.front = this.rear = null;
this.size = 0;
}
public void enqueue(E element) {
OneWayNode<E> n = OneWayNode<E>(element, null);
if (this.isEmpty())
this.rear = this.front = n;
else {
this.rear.next = n;
this.rear = n;
}
this.size += 1;
}
public E dequeue() {
E element = this.front.data;
this.front = this.front.next;
this.size -= 1;
if (this.isEmpty())
this.rear = null;
return element;
}
public E peek() {
return this.front.data;
}
public boolean isEmpty() {
return this.size() == 0;
}
public int size() {
return this.size == 0;
}
protected class OneWayNode<E> {
protected E data;
protected OneWayNode<E> next;
protected OneWayNode(E data, OneWayNode next) {
this.data = data;
this.next = next;
}
}
}
La forma per definir una subclasse és
public class <nom de la subclasse> extends <nom de la classe pare>{
<variables i mètodes>
}
La classe LinkedPriorityQueue és una subclasse de LinkedQueue. La nova classe inclou un constructor i el mètode enqueue. El constructor de LinkedPriorityQueue és opcional, però crida explícitament el constructor a la seva classe pare, utilitzant l’instrucció
super();
Això fa que la classe pare inicialitzi les seves variables d’instància.
El nou mètode enqueue primer comprova si hi ha una cua buida i, si això és cert, crida el mètode enqueue a la classe pare utilitzant la forma:
super.<nom del mètode>(<arguments>)
En cas contrari, el mètode busca la posició adequada del nou element i l’insereix allà. Com que la part frontal de la cua es troba al capdavant de l’estructura enllaçada, la cerca només s’atura quan l’element entrant és estrictament menor que l’element de la cua. Per tant, un element entrant es col·loca darrere de tots els elements de la mateixa prioritat.
Els elements d’una cua de prioritat han de ser comparables. Per tant, el tipus d’element de la classe LinkedPriorityQueue s’especifica com a Comparable. La sintaxi per relacionar els tipus d’element LinkedPriorityQueue i LinkedQueue a la capçalera de la classe és una mica complicada, però fa la feina.
Aquí teniu la definició de LinkedPriorityQueue:
import java.util.*;
public class LinkedPriorityQueue<E extends Comparable>
extends LinkedQueue<E> {
public LinkedPriorityQueue() {
super();
}
public void enqueue(E element) {
if (this.isEmpty())
super.enqueue(element);
else {
OneWayNode<E> probe = this.front;
OneWayNode<E> trailer = null;
while (true) {
if (probe == null ||
element.compareTo(probe.data) < 0)
break;
trailer = probe;
probe = probe.next;
}
if (probe == null) // final de la cua
this.rear.next = new OneWayNode<E>(element, null);
this.rear = this.rear.next;
else if (probe == this.front) // començament de la cua
this.front = new OneWayNode<E>(element, this.front.next);
else // al mig
trailer.next = new OneWayNode<E>(element, probe);
this.size += 1;
}
}
}
Classes abstractes
Quan dues o més classes contenen codi redundant, es pot factoritzar en una classe pare comuna. Aquesta classe normalment s’anomena classe abstracta, perquè no està instanciada.
Considereu dues implementacions de piles, ArrayStack i LinkedStack. Els seus contenidors interns d’elements són diferents, així com els seus mètodes per afegir, eliminar i iterar a través d’aquestes estructures. Tanmateix, si suposem que cada pila fa un seguiment de la seva mida de la mateixa manera, els mètodes len i isEmpty seran els mateixos per a qualsevol implementació. I si suposem que cada implementació inclou un mètode iter, podem implementar diversos altres mètodes, com ara str i addAll, només una vegada per a totes les piles.
L’exemple següent defineix una classe AbstractStack que inclou la informació comuna a totes les implementacions de pila. Per accedir a aquests recursos, una implementació de pila, com ara ArrayStack o LinkedStack, simplement estén AbstractStack. Aquesta classe s’especifica com a abstract amb la paraula reservada abstract. La classe també ha d’incloure tots els mètodes de la interfície TrueStack. Els mètodes que s’han de definir a les subclasses s’especifiquen com a mètodes abstractes, de nou amb la paraula reservada abstracte.
import java.util.*;
abstract public AbstractStack<E> implements TrueStack<E> {
protected int size, modCount;
public AbstractStack() {
this.size = 0;
this.modCount = 0;
}
public boolean empty() {
return this.size() == 0;
}
public int size() {
return size;
}
public String toString() {
String result = '';
for (E element : this)
result += str(element) + '\n';
return result;
}
public void addAll(Collection<E> col) {
for (E element : col)
this.push(element);
}
abstract public E peek();
abstract public E pop();
abstract public void push(E element);
abstract public Iterator<E> iterator();
}
AbstractStack és responsable d’inicialitzar dues variables d’instància, size i modCount. Cal tenir en compte que el mètode toString utilitza un bucle for sobre això, cosa que implica que l’objecte de pila és iterable. En conseqüència, cada implementació de pila ha d’incloure la seva pròpia definició del mètode iterator. Finalment, cal tenir en compte que el mètode addAll copia elements de qualsevol objecte Collection a la pila. Qualsevol objecte Collection reconeix el mètode iterator i, per tant, es pot utilitzar amb un bucle for.
La primera implementació, ArrayStack, inclou dos constructors. Un permet al programador especificar un objecte Collection com a argument. Aquest objecte es passa al mètode addAll per copiar els seus elements a la nova pila. També es pot cridar addAll per afegir diversos elements en qualsevol moment posterior. ArrayStack també és responsable de mantenir la seqüència d’elements. Per tant, inclou les definicions de peek, pop, push i iterator.
import java.util.*;
public class ArrayStack<E> extends AbstractStack<E> {
private List<E> list;
public ArrayStack() {
super();
this.list = new ArrayList<E>();
}
public ArrayStack(Collection<E> col) {
this();
this.addAll(col);
}
public E peek() {
if (this.isEmpty())
throw new EmptyStackException();
return list.get(this.size() - 1);
}
public E pop() {
if (this.isEmpty())
throw new EmptyStackException();
this.size -= 1;
this.modCount += 1;
return this.list.remove(this.size());
}
public void push(E element) {
this.list.add(element);
this.size += 1;
this.modCount += 1;
}
public Iterator<E> iterator() {
return new StackIterator<E>();
}
private class StackIterator<E> implements Iterator<E> {
private int curPos;
private int curModCount;
private StackIterator() {
this.curPos = this.size() - 1;
this.curModCount = modCount;
}
public boolean hasNext() {
return curPos >= 0;
}
public E next() {
if (! this.hasNext()
throw new IllegalStateException();
if (this.curModCount != modCount)
throw new ConcurrentModificationException()
E data = list.get(this.curPos);
this.curPos -= 1;
return data;
}
public void remove() {
throw new UnsupportedOperationException();
}
}
}
Els mateixos mètodes es defineixen a LinkedStack, però és clar que accedeixen a elements en un tipus d’estructura interna molt diferent.
import java.util.iterator;
public class LinkedStack<E> extends AbstractStack<E> {
private OneWayNode<E> items;
public LinkedStack() {
super();
this.items = null;
}
public LinkedStack(Collection<E> col) {
this();
this.addAll(col);
}
public void push(E element) {
this.items = new OneWayNode<E>(element, items);
this.size += 1;
this.modCount += 1;
}
public E pop() {
E element = this.items.data;
this.items = this.items.next;
this.size -= 1;
this.modCount += 1;
return element;
}
public E peek() {
return this.items.data;
}
public Iterator<E> iterator() {
return new StackIterator<E>();
}
private class StackIterator<E> implements Iterator<E> {
private OneWayNode curPos;
private int curModCount;
private StackIterator() {
this.curPos = items;
this.curModCount = modCount;
}
public boolean hasNext() {
return curPos != null;
}
public E next() {
if (! this.hasNext()
throw new IllegalStateException();
if (this.curModCount != modCount)
throw new ConcurrentModificationException()
E data = this.curPos.data;
this.curPos = this.curPos.next();
return data;
}
public void remove() {
throw new UnsupportedOperationException();
}
}
private class OneWayNode<E> {
private E data;
private OneWayNode next;
private OneWayNode(E data, OneWayNode next) {
this.data = data;
This.next = next;
}
}
}
Gestió d’errors
Java utilitza el concepte d’excepcions per manejar errors i situacions excepcionals durant l’execució del programa. Les excepcions es gestionen amb blocs try, catch i finally, i es poden generar manualment amb throw.
try {
int x = Integer.parseInt("abc"); // pot generar NumberFormatException
int resultat = 10 / x; // pot generar ArithmeticException
} catch (NumberFormatException e) {
System.out.println("No s'ha introduït un nombre vàlid.");
} catch (ArithmeticException e) {
System.out.println("No es pot dividir per zero.");
} finally {
System.out.println("Aquest bloc s'executa sempre.");
}
try: inclou el codi que pot llençar una excepció.catch: captura i gestiona excepcions específiques. Es poden tenir diversos blocscatch.finally: (opcional) conté codi que s’executa sempre, hagi o no hagut excepció.- L’objecte d’excepció capturat es pot utilitzar per obtenir informació addicional (
e.getMessage()).
Es poden generar excepcions manualment utilitzant throw per indicar errors específics:
public static int divideix(int a, int b) {
if (b == 0) {
throw new IllegalArgumentException("No es pot dividir per zero.");
}
return a / b;
}
Si un mètode pot llençar una excepció que no captura, cal declarar-ho amb throws:
public static void llegirFitxer(String nomFitxer) throws IOException {
// codi que pot llençar IOException
}
Excepcions comunes en Java:
| Excepció | Quan apareix |
|---|---|
NumberFormatException | Conversió de cadena a número no vàlida |
ArithmeticException | Operació aritmètica invàlida (ex. dividir per zero) |
NullPointerException | Accedir a un objecte nul |
ArrayIndexOutOfBoundsException | Índex fora de rang en un array |
IOException | Errors d’entrada/sortida (fitxers, xarxa) |
Referències
Recursivitat
- Conceptes
- Implementació
- Model d’execució
- Verificació dels mètodes recursius
- Altres activitats
- Activitats gràfiques
Conceptes

La recursivitat permet resoldre problemes en dos passos:
- Fent versions més petites del problema, repetidament, fins que tenim una mida de solució senzilla (obrim les nines).
- Combinant les solucions i s’obté la solució al problema inicial (les tornem a tancar)
La recursivitat és una eina poderosa, però pot ser menys eficient que solucions iteratives del mateix problema. Però a vegades és més senzill.
RECURSIVITAT = tipus de DESCOMPOSICIÓ de problemes
Exemple de recursivitat: factorial
En matemàtiques, el factorial n! és el número de permutacions de n elements:
n! = n * (n - 1) * (n - 2) * … * 2 * 1
Però també es por definir recursivament:
n! = 1, si n = 0 (cas base)
n! = n * (n - 1)! si n > 0 (cas general)
Per exemple:
1) 4! = 4 * 3!
2) 3! = 3 * 2!
3) 2! = 2 * 1!
4) 1! = 1 * 0!
5) 0! = 1 (cas base)
6) 1! = 1 * 0! = 1 * 1 = 1
7) 2! = 2 * 1! = 2 * 1 = 2
8) 3! = 3 * 2! = 3 * 2 = 6
9) 4! = 4 * 3! = 4 * 6 = 24
Implementació
A l’hora de decidir-se per una versió recursiva respecte d’una iterativa:
- S’ha de considerar si la solució recursiva és clara i eficient
- L’eficiència es pot mesurar en temps i espai (memòria)
- Algunes solucions són per definició ineficients
- Veure l’ordre de complexitat de l’algorisme recursiu i comparar-lo amb la seva versió iterativa
Activitat: factorial
Pensa com es podria implementar la funció factorial.
A Java, un mètode pot invocar un altre, fins i tot a si mateix!
Implementació del factorial:
public static int factorial(int n) {
if (n == 0) { // cas base
return 1;
}
else { // cas general
return n * factorial(n - 1);
}
}
Pregunta: què passaria si no hi hagués un cas base?
Activitat: canvi de base
Donat un número decimal, mostrar el seu valor binari.
Pista: divideix el número per 2 consecutivament per calcular cada xifra binària.
- Explica com es pot calcular a mà
- Pensa en un algorisme iteratiu
- Pensa com es podria convertir en recursiu
- Codifica l’algorisme
Activitat: potència
Donats dos nombres sencers b (base) i n (exponent), calcula b elevat a n, tenint en compte que b elevat a 0 = 1.
Fes-te les següents preguntes:
- Defineix el mètode que farà el càlcul: valor de retorn i paràmetres
- Com es pot calcular iterativament?
- Quants paràmetres tindrà el mètode que realitzi en càlcul?
Per fer la versió recursiva, pensa en els possibles valors de n per trobar
- el cas base: per quina n podem acabar la recursió?
- el cas general: com calcular la potència per un cas més petit?
Model d’execució
processador (CPU) ⇒ comptador de programa (PC)
molles de pa ⇒ permet tornar enrere al lloc on es va produir la crida ⇒ STACK
A la memòria tenim: codi i dades.
A les dades, hi ha dos espais de memòria importants:
- el heap: és un espai on viuen els objectes i on es fa el garbage collection
- el stack: és una pila (LIFO) on viuen les invocacions de mètodes d’un fil (thread). L’últim és el mètode que s’executa actualment.
Quan s’invoca un mètode s’afegeix un element (stack frame) al stack que conté:
- Els paràmetres
- Les variables locals creades
- L’adreça de retorn del mètode (cap, per al primer stack frame)
Quan es crea un objecte s’afegeix al heap, i es crea una referència a ell. Aquest espai es pot recuperar quan no hi ha més referències (garbage collection).
Exemple: quadrat
Al següent codi es calcula el quadrat d’un número.

Aquest seria el contingut dels stack frames a la línia 20 (PC = 20), si l’argument és “5” (stack frame):
| Paràmetres | Retorn | Variables locals |
|---|---|---|
| num = 5 | 15 | calc = 25 |
| args = { “5” } | (exit) | n = 5, nal2 = ? |
Exemple: hipotenusa
Al següent codi es fa el càlcul de la hipotenusa. Imaginem que tenim tres stack frames:

Com seria el contingut dels stack frames si el PC és 25, s’està calculant c1al2 i els arguments “3” i “5”?
Verificació dels mètodes recursius
Per veure si una solució recursiva funciona, la resposta ha de ser “sí” a aquestes tres preguntes:
- (Cas base) Hi ha una forma no recursiva de sortir del mètode, i el mètode funciona bé per aquest cas?
- (Cas més petit): Cada crida recursiva al mètode, involucra un cas més petit del problema original, que finalment arribarà al cas base?
- (Cas general): Si les crides recursives funcionen, funcionarà també el mètode?
Activitat. Per al factorial:
- Sí, el cas base passa si n = 0, i es retorna 1 sense més crides recursives.
- Sí, el cas més petit és factorial(n - 1), que finalment arribarà a factorial(0).
- Sí, el cas general és n * factorial(n-1), i si la crida recursiva funciona per n - 1 llavors ha de funcionar per a la fórmula.
Execució del factorial
Aquest seria el codi de l’execució del factorial:

Aquests serien els stack frames:

Activitat: depuració de mètodes recursius
- Depura amb el teu IDE el mètode factorial
- Afegeix un breakpoint al començament del mètode
- Comprova el valor de n i el flux de control en funció del seu valor
- Mira la finestra de depuració i comprova el call stack
- Per cada call stack, mira el valor de n
Execució de la conversió a binari
Conversió a binari de 13 (1101):

Altres activitats
Aquestes són altres possibles activitats:
- Si un número és parell o senar (recursivitat creuada o indirecta)
- Mostrar les permutacions per N lletres (veure imatge)
- Els números de Fibonacci (recursivitat múltiple)
- Els números d’Ackermann (recursivitat niada)
- Les torres d’Hanoi
Els nombres de Fibonacci són una successió matemàtica de nombres naturals tal que cada un dels seus termes és igual a la suma dels dos anteriors. Suposa F(0) = 0 i F(1) = 1,
Els nombres d’Ackermann és una funció recursiva amb dos nombres naturals com arguments que retorna un altre natural. Si la funció és A(m, n), el resultat és:
- n+1, si m = 0
- A(m-1, 1), si m > 0 i n = 0
- A(m-1, A(m, n-1)), si m > 0 i n > 0

Les torres d’Hanoi consisteixen en tres varetes verticals i un nombre indeterminat de discs de mides diferents que determinen la complexitat de la solució.
A l’inici estan col·locats de més gran a més petit en la primera vareta.
El joc consisteix a passar tots els discs a la tercera vareta tenint en compte que només es pot canviar de vareta un disc cada vegada i que mai no podem tenir un disc col·locat sobre un que sigui més petit.

Activitats gràfiques
Per a aquesta secció, pots utilitzar una llibreria gràfica de java basada en AWT o similar. Per exemple, StdDraw.
StdDaw:
- És una classe amb mètodes estàtics
- Per defecte, crea un espai amb coordenades (0,0) a (1,1)
- Copieu l’arxiu StdDraw.java al vostre projecte i proveu això:
Per exemple, la pots provar amb:
public class TestStdDraw {
public static void main(String[] args) {
StdDraw.setPenRadius(0.05);
StdDraw.setPenColor(StdDraw.BLUE);
StdDraw.point(0.5, 0.5);
StdDraw.setPenColor(StdDraw.MAGENTA);
StdDraw.line(0.2, 0.2, 0.8, 0.2);
}
}
Addicionalment, pots utilitzar una abstracció molt comuna, la tortuga. Aquesta és una possible definició del contracte que hauries d’implementar:
public interface Turtle {
void up(); // Sets the turtle up (not drawing)
void down(); // Sets the turtle down (drawing)
void left(double delta); // Turn delta degrees to the left
void right(double delta); // Turn delta degrees to the right
void forward(double step); // Go forward (drawing if it's down)
void backward(double step); // Go backward (drawing if it's down).
}
Un exercici interessant és dibuixar polígons estrella.
L’angle en graus per dibuixar els polígons d’aquestes estrelles es pot calcular com:
angle = 180 - (180 * p - 360 * q) / p
on p és el nombre de vertex i q el nombre de voltes.
Dibuixeu les següents figures recursives:
- Espiral
- Arbre recursiu
- Htree
- Triangle Sierpinski
- Espiral amb els costats de colors
- Espiral amb vuit costats
- Floc de neu de Koch


Ordenació
Algorismes
Un algorisme és una especificació no ambigua de com resoldre una classe de problemes. Exemple: sortir de casa.
- Especificació (mitjançant un diagrama)
- No ambigua (llenguatge clar: plou/no plou)
- Resoldre (hi ha un resultat clar: sortir)
- Classe de problemes (no només un)
Encara no parlem de programes!
algorisme + dades ⇒ programes
Activitat: algorisme per trobar el nombre més gran d’una llista desordenada (sense codi)
Complexitat
La notació O(…) gran permet classificar els algorismes en funció dels requisits de temps o espai quan les dades d’entrada creixen. Descriu el pitjor escenari.

Exemple: un dels n alumnes de la classe ha amagat la meva cartera
- O(1): sé qui és l’alumne que ha amagat la cartera ⇒ li pregunto
- O(n): només un alumne, que ha amagat la cartera, sap on és ⇒ he d’anar un a un preguntant
- O(log n): tots els alumnes saben qui ha amagat la cartera, però només m’ho diran si endevino el nom ⇒ vaig dividint la classe i pregunto a tots si és a la dreta o a la esquerra
- O(n2): a tota la classe, només un alumne sap a quina taula és la cartera ⇒ pregunto a cada alumne i li pregunto per la resta d’alumnes
Activitat: quina és la complexitat de l’algorisme per trobar el nombre més gran d’una llista desordenada? I si fos una llista ordenada?
Estructures de dades
És una forma d’organitzar i emmagatzemar dades per que que es puguin accedir i (opcionalment) modificar eficientment.
quines operacions (queries) tenim? ⇒ quina estructura utilitzar
Operació més habitual: trobar un registre que tingui un camp igual a un cert valor.
Alguns tipus de dades:
- primitius: booleans, nombres sencers i de coma flotant, caràcters, etc.
- compostos: arrays (ordenats i desordenats)
- abstractes: llistes (ordenades i desordenades), col·leccions, arrays associatius, conjunts, piles, cues, arbres, grafs, etc.
Aquesta és la cerca segons el tipus de dades:
| Tipus | Operacions | Utilitat |
|---|---|---|
| array (desordenat) | accés i modificació per posició | iterar |
| array (ordenat) | accés i modificació per posició | iterar i cerca binària |
| llista (desordenada) | accés per posició, qualsevol modificació | iterar, modificar |
| llista (ordenada) | accés per posició, qualsevol modificació | iterar, modificar i cerca binària |
| pila | afegir i treure de dalt | molt específica |
| diccionari o mapa | afegir parella clau / valor, accés per clau | iterar, modificar i cerca per clau |
Cerca binària

Activitat
- Implementa un mètode que crei un array de N sencers, on N és un paràmetre. Utilitza new Random().nextInt()
- Ordena’l amb Arrays.sort()
- Dissenya l’algorisme de cerca binària per trobar la posició un nombre, si existeix
Anàlisi
Mètode iteratiu. Considera els conceptes en negreta:
Repetir, mentre no es trobi i quedin elements per buscar:
- Es calcula la posició de l’element del mig.
- Es compara el nombre buscat amb el contingut del mig
- Si és menor, buscar a partir del mig
- Si és major, buscar fins al mig
- Si és igual, l’hem trobat!
Reflexiona:
- Quines variables necessitaràs?
- Quants cops es repeteix cada bucle?
- Quines conseqüències té cada acció de l’algorisme.
Ordenació
Veure abans aquest vídeo.

Alguns algorismes senzills d’ordenació són:
- Inserció
- Selecció
- Bombolla
Reflexió: com podem ordenar un array?
Activitat: com faries una ordenació manualment? Descriu el teu algorisme perquè algú el pugui implementar en codi (taller).
Alguns algorismes coneguts:
- bubble sort - O(n2) ⇒ intercanviar contigus mentre hi hagi algun moviment
- selection sort - O(n2) ⇒ per cada ítem, busquem la seva posició i el movem
- merge sort - O(n log n) ⇒ dividim l’array en meitats, les ordenem i les barregem (té solució recursiva)
Veure aquest vídeo de com sonen diversos algorismes.
Activitat: implementa el selection sort.
Repetir, mentre hi hagi elements per ordenar:
- Es busca la posició del nombre més gran.
- Es compara amb el nombre a la darrera posició.
- Si és més gran, s’intercanvien.
- Si no, no es fa res
- Es repeteix, ignorant el darrer element.
Reflexiona:
- Quines variables necessitaràs?
- Quants cops es repeteix cada bucle?
- Quines conseqüències té cada acció de l’algorisme.
Col.leccions i mapes
Referències
Framework
El framework de col.leccions de Java permet emmagatzemar, obtenir, manipular i comunicar dades agregades. Conté els següents elements:
- Interfícies: són tipus de dades abstractes que representen col·leccions. Les interfícies permeten manipular les col·leccions independentment dels detalls de la seva representació. En llenguatges orientats a objectes, les interfícies formen generalment una jerarquia.
- Implementacions: Són les implementacions concretes de les interfícies de recollida. En essència, són estructures de dades reutilitzables.
- Algorismes: Són els mètodes que realitzen càlculs útils, com cercar i ordenar, en objectes que implementen interfícies de col·lecció. Es diu que els algoritmes són polimorfs: és a dir, es pot utilitzar el mateix mètode en moltes implementacions diferents de la interfície de col·lecció adequada. En essència, els algoritmes són funcionalitats reutilitzables.
A continuació es poden veure les interfícies principals.
- Collection : l’arrel de la jerarquia de col·leccions. Una col·lecció representa un grup d’objectes coneguts com els seus elements. La interfície de col·lecció és el denominador comú que totes les col·leccions implementen i s’utilitza per passar col·leccions i manipular-les quan es desitgi la màxima generalitat. Alguns tipus de col·leccions permeten duplicar elements, i d’altres no. Alguns estan ordenats i d’altres no ordenats. La plataforma Java no proporciona cap implementació directa d’aquesta interfície, però proporciona implementacions de subinterfícies més específiques, com ara Set and List.
- Set : una col·lecció que no pot contenir elements duplicats. Aquesta interfície modela l’abstracció de conjunts matemàtics i s’utilitza per representar conjunts, com ara les cartes que contenen una mà de pòquer, els cursos que configuren el programa d’un estudiant o els processos que s’executen en una màquina. Té una versió ordenada, SortedSet (per valor ascendent).
- List : una col·lecció ordenada (de vegades anomenada seqüència). Les llistes poden contenir elements duplicats. L’usuari d’una llista generalment té un control precís sobre on s’insereix cada element a la llista i pot accedir a elements mitjançant l’índex d’enters (posició). Si heu utilitzat Vector, coneixeu el sabor general de la llista. Consulteu també la secció La interfície de llista.
- Queue : una col·lecció usada per contenir diversos elements abans del processament. A més de les operacions bàsiques de recollida, una cua proporciona operacions addicionals d’inserció, extracció i inspecció. Les cues normalment, però no necessàriament, ordenen elements de manera FIFO (first-in, first-out).
- Deque : és una cua de doble final, i per tant permet inserir, extraure i inspeccionar elements als dos punts. Deques es pot utilitzar tant com FIFO (primer ingrés, primer sortida) com LIFO (darrera entrada, primer sortida).
- Map : un objecte que assigna mapes de valors. Un mapa no pot contenir claus duplicades; cada tecla pot associar com a màxim un valor. Si heu utilitzat Hashtable, ja coneixeu els fonaments bàsics de Map. Consulteu també la secció La interfície del mapa. Té una versió ordenada, SortedMap (per clau ascendent).
Les col·leccions utilitzen el concepte de genèrics de Java. Bàsicament, permet definir el tipus de l’element de les col·leccions com un paràmetre. Per exemple, per a la collecció de tipus List, la definició a la documentació de Java és:
Interface List<E>
Això significa que List és una llista d’elements (E) de tipus parametritzable. Per tant, podem utilitzar List per qualsevol classe (no tipus primitiu).
Genèrics
- Mètodes genèrics
- Paràmetres de tipus delimitat
- Classes genèriques
- Implementacions genèriques
- Comodins
Els genèrics permeten que els tipus (classes i interfícies) siguin paràmetres a l’hora de definir classes, interfícies i mètodes. Un cop que s’instancien, els paràmetres són substituïts pels tipus reals.
Beneficis:
- Controls de tipus més forts a la compilació.
Un compilador Java aplica una verificació de tipus forta al codi genèric i emet errors si el codi viola la seguretat del tipus. La correcció d’errors en temps de compilació és més fàcil que arreglar errors d’execució, que poden ser difícils de trobar.
- Eliminació de casts. El següent codi requereix tipus:
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);
Quan es reescriu amb genèrics, no el requereix:
List<String> list = new ArrayList<String>();
list.add("hello");
String s = list.get(0); // no cast
- Permet als programadors implementar algoritmes genèrics.
Mitjançant l’ús de genèrics, els programadors poden implementar algoritmes genèrics que treballin en col·leccions de diferents tipus, es poden personalitzar i són de tipus segur i més fàcil de llegir.
Des de Java 7 podem estalviar-nos la definició del paràmetre de tipus del constructor, ja que Java l’infereix:
List<String> list = new ArrayList<>();
Mètodes genèrics
- Totes les declaracions genèriques del mètode tenen una secció de paràmetre tipus delimitada per claudàtors d’angle (<i>) que precedeix el tipus de retorn del mètode (
<E>en el següent exemple). - Cada secció de paràmetres de tipus conté un o més paràmetres de tipus separats per comes. Un paràmetre tipus, també conegut com a variable de tipus, és un identificador que especifica un nom de tipus genèric.
- Els paràmetres de tipus es poden utilitzar per declarar el tipus de devolució i actuar com a marcadors per als tipus d’arguments passats al mètode genèric, que es coneixen com a arguments de tipus reals.
- El cos d’un mètode genèric es declara com el de qualsevol altre mètode. Tingueu en compte que els paràmetres de tipus només poden representar tipus de referència, no tipus primitius (com int, double i char).
public <E> void printArray(E[] inputArray) {
// Display array elements
for (E element : inputArray) {
System.out.printf("%s ", element);
}
System.out.println();
}
Paràmetres de tipus delimitat
Hi pot haver moments en què voldreu restringir els tipus de tipus que es permeten passar a un tipus de paràmetre. Per exemple, un mètode que opera sobre números només pot voler acceptar instàncies de Number o de les seves subclasses.
Per declarar un paràmetre de tipus delimitat, enumereu el nom del paràmetre del tipus, seguit de la paraula clau extends, seguit tipus delimitant.
public <T extends Comparable<T>> T maximum(T x, T y, T z) {
T max = x; // assume x is initially the largest
if (y.compareTo(max) > 0) {
max = y; // y is the largest so far
}
if (z.compareTo(max) > 0) {
max = z; // z is the largest now
}
return max; // returns the largest object
}
Es poden tenir múltiples tipus delimitats:
<T extends Type1 & Type2>
Classes genèriques
Una declaració de classe genèrica sembla una declaració de classe no genèrica, tret que el nom de classe sigui seguit per una secció de paràmetre tipus.
Com en els mètodes genèrics, la secció de paràmetres de tipus d’una classe genèrica pot tenir un o més paràmetres de tipus separats per comes. Aquestes classes es coneixen com a classes parametrizades o tipus parametrizats perquè accepten un o més paràmetres.
public class Box<T> {
private T t;
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
}
Implementacions genèriques
Les interfícies genèriques es poden implementar bàsicament de dues formes: de forma genèrica o no, utilitzant un tipus específic.
Suposem que tenim la següent interfície genèrica:
interface Container<T> {
T getValue();
}
Una implementació genèrica manté el paràmetre genèric:
public class ContainerImpl<T> implements Container<T> {
private T t;
public ContainerImpl(T t) {
this.t = t;
}
@Override
public T getValue() {
return t;
}
}
Una implementació no genèrica utilitza un tipus específic a la interfície:
public class LongContainerImpl implements Container<Long> {
private Long l;
public LongContainerImpl(Long l) {
this.l = l;
}
@Override
public Long getValue() {
return l;
}
}
Així es podrien utilitzar aquestes dues implementacions:
Container<String> strContainer = new ContainerImpl<>("test");
System.out.println(strContainer.getValue().toUpperCase());
Container<Long> longContainer = new LongContainerImpl(12L);
System.out.println(longContainer.getValue() * 2);
Comodins
El comodí (?) permet referir-se a un tipus desconegut. Habitualment s’utilitza com a tipus d’un paràmetre, camp o variable local. Tenim diferents tipus:
- comodins delimitats per dalt:
? extends Foo. El paràmetre permet Foo o qualsevol subtipus de Foo. Utilitzat si és un paràmetre només d’entrada d’un mètode.
public static void process(List<? extends Foo> list) {
for (Foo elem: list) {
// ...
}
}
- comodins no delimitats:
?. El paràmetre permet qualsevol tipus. - comodins delimitats per baix:
? super Foo. Permet Foo o qualsevol supertipus de Foo. Utilitzat si és un paràmetre només de sortida d’un mètode.
Ús de col.leccions
Tota Collection és un Iterable, la qual cosa serveix per iterar qualsevol List, Set o Queue.
Per iterar tenim el mètode iterator():
Iterator<Integer> iterator = list.iterator();
while(iterator.hasNext()) {
Integer nextInt = iterator.next();
}
També es pot utilitzar el format for-each loop:
for (Integer nextInt: list) {
// ...
}
Hi ha dos mètodes d’Object que utilitzem en relació a les col·leccions, i que sovint cal sobreescriure:
public int hashCode(): retorna un sencer diferent per a cada objecte. Siequals()retornatrue, han de retornar el mateix sencer. S’utilitza per a inserir i cercar a col·leccions que utilitzen taules hash.public boolean equals(Object obj): retornatruesi els dos objectes es consideren iguals. Si es retornatrue, cal que també hashCode() retorni el mateix sencer.Objectté aquesta implementació per defecte:this == obj, que significa que són el mateix objecte. Però sovint volem retornartrueencara que no es tracti del mateix objecte. Per exemple,Integerretornatruesi els dos objectes contenen el mateix valorint.
Implementació típica de equals():
public boolean equals(Object o){
if (o == null)
return false;
if (!(o instanceof Treballador))
return false;
Treballador altre = (Treballador) o;
return this.treballadorId == altre.treballadorId;
}
Implementació típica de hashcode():
public int hashCode(){
return (int) treballadorId;
}
Implementació alternativa de hashcode() utilitzant Objects:
public int hashCode(){
return Objects.hash(treballadorId); // llista de camps de l'objecte
}
Altres mètodes Collection<E>:
boolean add(E e);void clear();boolean contains(Object o);boolean isEmpty();boolean remove(Object o);int size()
El mètode contains() utilitza el mètode equals() per veure si existeix l’element. Per tant, depèn de la implementació de cada classe. En el cas de Integer, la documentació diu:
- The result is
trueif and only if the argument is notnulland is anIntegerobject that contains the sameintvalue as this object.
Per tant, són iguals dos objectes Integer que contenen el mateix valor sencer. Object implementa el mètode equals() com la igualtat (this == o). Compte, perquè si es pretén sobreescriure el mètode equals(), sempre s’ha de sobreescriure també el mètode hashcode(). Veure Objects.hash().
List<E> afegeix operacions per posicions:
void add(int index, E element)E get(int index)E set(int index, E element)E remove(int index)
La implementació més clàsica és la de ArrayList. LinkedList podria ser interessant si s’insereixen elements al començament amb freqüència.
Set<E> és una col·lecció que no conté repeticions. O sigui, no hi ha dos elements tals que e1.equals(e2). No conté mètodes addicionals respecte Collection.
Tenim tres implementacions:
HashSetutilitza elhashCode()de la clau per a optimitzar l’accés als elements.TreeSetutilitza una estructura en arbre navegable segons l’ordre dels elements, que han de ser comparables (implementen la interfície Comparable). Es basa enTreeMap.LinkedHashSetpermet navegar els elements segons l’ordre d’inserció.
Queue<E> és una col·lecció amb el concepte associat de “cap” i “cua”: lloc per on es treuen i s’afegeixen els elements:
boolean add(E e): afegeix un element a la cua (amb excepció).boolean offer(E e): afegeix un element a la cua.E remove():esborra l’últim element al cap (amb excepció).E poll():esborra l’últim element al cap.E element():examina l’’últim element al cap (amb excepció).E peek():examina l’últim element al cap.
Com es veu, hi ha dos mètodes per cada operació (afegir, esborrar, examinar), una amb excepció i una altra sense.
Dues de les possibles implementacions:
LinkedList: la implementació més habitual.PriorityQueue: permet que la cua estigui ordenada mitjançant un comparador, en lloc d’utilitzar l’ordre en que s’afegeixen els elements.
Deque<E> és una Collection i també una Queue<E>. Permet afegir i treure elements als dos costats de la col.lecció, ‘first’ i ‘last’. També permet implementar una pila (classe deprecada Stack) amb els mètodes:
void push(E e): afegeix un element a la pila (cap)E pop(): treu un element de la pila (cap)E peek(): examina l’element de la pila (cap)
LinkedList també implementa Deque.
Ús de mapes
Els mapes són estructures de dades dinàmiques que contenen correspondències entre parelles de clau i valor.
Map<K, V> té aquestes operacions principals:
int size()boolean isEmpty()boolean containsKey(Object)V put(K, V)V remove(Object)void clear()Set<K> keySet()Collection<V> values()Set<Entry<K, V>> entrySet()
El tipus Entry<K, V> és una parella clau/valor immutable amb els mètodes:
K getKey()V getValue()
Les tres principals implementacions són:
HashMaputilitza elhashCode()de la clau per a optimitzar l’accés als elements.TreeMappermet navegar els elements segons l’ordre natural d’aquests, que han de ser comparables (implementen la interfície Comparable).LinkedHashMap: permet navegar els elements segons l’ordre d’inserció.
Comparació d’objectes
Comparables
Una classe és comparable si permet que les seves instàncies puguin ser comparades per poder ordenar-les entre sí. Totes les classes embolcall dels tipus primitius ho són (String, Integer, Double…). Les classes comparables implementen la interfície Comparable<T>.
La interfície té només un mètode:
int compareTo(T o)
El valor retornat pot ser:
- negatiu, si aquest objecte és menor que o.
- 0, si els dos són iguals.
- positiu, si aquest objecte és major que o.
Les claus de la implementació TreeMap i els objectes d’un TreeSet han de ser comparables. Això permet poder navegar-los segons el seu ordre.
Comparadors
Els comparadors permeten comparar dos objectes d’un tipus T per poder ordenar-los. Són objectes que implementen la interfície Comparator<T>.
La interfície té només un mètode:
int compare(T o1, T o2)
El valor retornat pot ser:
- negatiu, si o1 és menor que o2.
- 0, si els dos són iguals.
- positiu, si o1 és major que o2.
Alguns algorismes permeten utilitzar comparadors per ordenar col·leccions (veure la secció Algorismes). En general, és millor comparar fent la classe comparable, però si no podem o volem modificar el seu codi, podem crear un comparador.
Algorismes
La majoria d’algorismes operen en llistes (List), i alguns a Collection:
static <T extends Comparable<? super T>> void sort(List<T> list): ordena una llista d’elements comparables.static <T> void sort(List<T> list, Comparator<? super T> c): ordena una llista d’elements, utilitzant un comparador.static void shuffle(List<?> list): desordena una llista- Cinc algorismes més per manipular objectes d’una llista:
reverse, fill, copy, swapiaddAll static <T> int binarySearch(List<? extends Comparable<? super T>> list, T key): cerca binària a una llista- Sobre collections:
frequency, disjoint, minimax
Stream API
Java té una API que permet processar seqüències d’objectes mitjançant operacions que es poden afegir a una canonada. Un Stream es genera a partir de col.leccions, arrays o canals d’E/S, i no és modificable: només podem canviar el resultat mitjançant les operacions de la canonada.
Tenim bàsicament dos tipus d’operacions: les intermèdies i les terminals.
- Operacions intermèdies: permeten afegir operacions addicionals al darrere.
- Operacions terminals: marquen el final del stream i retornen el resultat.
Les operacions s’encadenen en la canonada mitjançant crides successives que utilitzen expressions lambda. En el següent exemple, s’utilitza l’operació terminal forEach() amb una expressió lambda de tipus Consumer.
List<Integer> list = Arrays.asList(3, 2, 5, 4, 1);
Stream<Integer> stream = list.stream();
Consumer<Integer> consumer = (number) -> { System.out.println(number); };
stream.forEach(consumer);
Operacions intermèdies:
-
map: permet aplicar una
Functionper a canviar el tipus delStreamde T a R<R> Stream<R> map(Function<? super T, ? extends R> mapper) -
flatMap: permet aplicar una
Functionper a convertir cada T als continguts d’un stream de R<R> Stream<R> flatMap(Function<? super T,? extends Stream<? extends R>> mapper) -
filter: permet modificar el stream filtrant els elements del
Streamamb unPredicateStream<T> filter(Predicate<? super T> predicate) -
sorted: permet ordenar els elements amb l’ordre natural (han de ser Comparable)
Stream<T> sorted()
Operacions terminals:
-
collect: permet reduir els elements amb un
Collector<T, A, R>: T és l’entrada, A l’acumulació i R el tipus resultat. Tenim collectors a la classeCollectors.<R,A> R collect(Collector<? super T,A,R> collector) -
forEach: permet executar una acció per cada element.
void forEach(Consumer<? super T> action) -
reduce: permet reduir els elements d’aquest stream utilitzant un
BinaryOperatorque permet ferT apply(T, T).Optional<T> reduce(BinaryOperator<T> accumulator)
Exemples:
// crear llista de sencers
List<Integer> number = Arrays.asList(2, 3, 4, 5);
// map
List<Integer> square = number.stream()
.map(x -> x * x)
.collect(Collectors.toList());
System.out.println(square);
// flatMap
List<Integer> square2 = number.stream()
.flatMap(x -> Stream.of(x, x*2))
.collect(Collectors.toList());
System.out.println(square2);
// crear llista de strings
List<String> names = Arrays.asList("Reflection", "Collection", "Stream");
// filter
List<String> result = names.stream()
.filter(s -> s.startsWith("S"))
.collect(Collectors.toList());
System.out.println(result);
// sorted
List<String> show = names.stream()
.sorted()
.collect(Collectors.toList());
System.out.println(show);
// crear llista de sencers
List<Integer> numbers = Arrays.asList(2, 3, 4, 5, 2);
// collect retorna un Set
Set<Integer> squareSet = numbers.stream()
.map(x -> x * x)
.collect(Collectors.toSet());
System.out.println(squareSet);
// forEach
number.stream()
.map(x -> x * x)
.forEach(y -> System.out.println(y));
// reduce
int even = number.stream()
.filter(x -> x % 2 == 0)
.reduce(0, (ans, i) -> ans + i);
System.out.println(even);
Expressions regulars
Les expressions regulars ens permeten trobar patrons dins de cadenes de text. Podem utilitzar-les per validar dades, fer cerques i substituir-les. Estan implantades a molts gestors de bases de dades.
A Java, tenim un mètode de Pattern que permet comprovar si una cadena compleix un patró, retornant true en cas positiu. L’ús més general es fa utilitzant les classes Pattern i Matcher.
// regex: 0 a N lletres a i una b
boolean b = Pattern.matches("a*b", "aaaaab");
// alternativament:
Pattern p = Pattern.compile("a*b");
Matcher m = p.matcher("aaaaab");
boolean b = m.matches();
El funcionament de les expressions regulars a Java s’explica a la documentació de la classe Pattern.
Una de les funcionalitats més interessants de les expressions regulars és la captura de grups. A la secció d’una expressió podem afegir parèntesi, i això voldrà dir que volem obtenir aquell grup de forma individual.
Pattern p = Pattern.compile("(a*)(b*)c");
Matcher m = p.matcher("aaabbc");
boolean b = m.matches(); // true
String group1 = m.group(1); // aaa
String group2 = m.group(2); // bb
Excepcions
Una excepció és un esdeveniment que passa durant l’execució d’un programari, i que trenca el flux normal d’execució d’instruccions.
Quan hi ha una excepció, el mètode on passa crea un objecte (excepció) i la llença a l’entorn d’execució (throws). Aquest objecte indica què ha passat i quin era l’estat quan ha passat. L’objecte de l’excepció inclou la llista de mètodes que s’havia cridat quan passa l’excepció, o call stack.
L’entorn d’execució busca un bloc de codi que pugui gestionar l’excepció. Aquesta cerca es fa des del mètode on es produeix l’excepció i en ordre invers de crida, fins arribar al mètode main. Quan es troba el codi que gestiona l’excepció (catch), l’entorn d’execució passa l’excepció al codi corresponent. Si no troba cap, el programa acaba.
La gestió de l’excepció és apropiada si el tipus de l’excepció coincideix amb el de la gestió de l’excepció.
Avantatges de les excepcions:
- Permeten separar el codi de gestió d’errors del codi normal.
- Permeten propagar els errors, tenint un punt comú per gestionar errors d’un cert tipus.
- Permet agrupar i diferenciar tipus d’errors, utilitzant gestors més o menys generals.
Referències
Tipus d’excepcions
- Checked: són les excepcions que cal gestionar obligatòriament. Es produeixen per condicions fora de l’abast del programa. Exemple:
FileNotFoundException. Hi ha dues formes de gestionar-les:- una sentència “try/catch“que gestioni l’excepció
try {
// codi que llença una excepció
} catch (SomeException e) {
// codi que gestiona l'excepció
}
- que el mètode especifiqui que pot llençar aquesta excepció (throws)
public void metode() throws SomeException {
// codi que llença una excepció
}
- Unchecked: són les excepcions que no cal gestionar. Habitualment, reflecteixen errors de lògica.
- Errors (tipus
Error): són esdeveniments excepcionals (del tipus Error) i habitualment irrecuperables. Exemple:OutOfMemoryError. - Excepcions de l’entorn d’execució (tipus
RuntimeException): són excepcionals i associats a l’aplicació. Exemple:NullPointerException.
- Errors (tipus
Gestió d’excepcions
Throws
Es pot tractar una excepció simplement fent throws:
public void metode() throws FileNotFoundException { ...
Try-catch
Podem gestionar l’excepció rellançant-la:
try {
catch (SomeException e} {
throw new AnotherException("un missatge");
}
O bé fent accions per recuperar-nos:
try {
catch (SomeException e} {
return VALOR_PER_DEFECTE;
}
Finally
Quan volem executar algun codi, passi o no passi l’excepció. Pot servir per alliberar recursos.
try {
// ...
} catch (SomeException e) {
// ...
} finally {
// el codi aqui sempre s'executarà
}
Try-with-resources
Permet treballar amb recursos, i tancar-los sense fer-ho explícitament al codi. Cal que el recurs implementi AutoCloseable.
try (AutoCloseableObject o = new AutoCloseableObject()) {
// ...
} catch (SomeException e) {
// ...
}
Multiples catch
Permet gestionar diverses excepcions. L’ordre és important: si tenim excepcions que comparteixen tipus, cal que estiguin les més específiques abans.
try {
// ...
} catch (SomeEspecificException e) {
// ...
} catch (LessEspecificException e) {
// ...
} catch (AnotherException e) {
// ...
}
Union
Permet gestionar diverses excepcions al mateix bloc.
try {
// ...
} catch (SomeException | SomeOtherException | AnotherException e) {
// ...
}
Excepcions pròpies
Habitualment, tenim diverses signatures per crear un objecte d’excepció. Tot i que els paràmetres són lliures (podem afegir els que ens ajudin a interpretar el nostre error particular), dos sovintegen:
- el missatge d’error, un String.
- una excepció, la causa. Permet enllaçar excepcions amb les seves causes (wrapping).
Per exemple, una excepció checked estenent Exception:
public class SomeException extends Exception {
public SomeException (String message, Throwable t) {
super(message, t);
}
}
Per exemple, una excepció unchecked estenent RuntimeException:
public class SomeException extends RuntimeException {
public SomeException (String message, Throwable t) {
super(message, t);
}
}
Llençant excepcions
Checked
Es pot llençar, i cal especificar-ho.
public void metode() throws SomeException {
// ...
throw new SomeException("some text");
}
Unchecked
No cal especificar-la:
public void metode() {
// ...
throw new SomeException("some text");
}
Rethrowing
Tornem a llençar l’excepció gestionada:
try {
// ...
} catch (SomeException e) {
throw e;
}
Wrapping
Fem un embolcall:
try {
// ...
} catch (SomeException e) {
throw new SomeOtherException("una explicació", e);
}
Herència
Quan sobreescribim un mètode, podem fer que la subclasse tingui una signatura menys arriscada, o sigui, sense el throws.
Veure la interpretació de les excepcions.
Indicacions
La tentació d’un programador podria ser utilitzar excepcions unchecked, ja que no cal gestionar-les ni declarar-les als mètodes.
Quan un mètode especifica una excepció a la seva signatura, està demanant a qui el crida què vol fer: si tornar a llençar o bé gestionar l’excepció. Les excepcions unchecked són el resultat d’esdeveniments no recuperables, i no té sentit que hagin de ser declarades (tot i que seria possible) per tot arreu, ja que el codi no seria clar.
La regla seria: si una crida pot recuperar-se d’una excepció, fes que sigui checked. Si no pot fer res, fes que sigui unchecked.
Alguns anti-patrons típics:
- Empassar-se excepcions
- Fer return a un finally
- Fer throw a un finally
- Utilitzar throw com a goto
Logging
El registre en logs (logging) permet organitzar d’una forma més óptima els missatges generats en temps d’execució per un programari, i és una alternativa millor que els System.out.println. Té els següents avantatges:
- Permet assignar una prioritat als missatges, i posteriorment filtrar-los. Tant de forma global com per paquets (packages) i classes.
- Permet reencaminar els missatges a diferents destins, com consola, fitxer, etc.
- Permet adaptar el format dels missatges, com per exemple en text, en XML o a base de dades.
Java conté a la seva llibreria base el paquet java.util.logging. Aquest paquet pot ser utilitzat de forma bàsica de la següent forma:
private static final Logger LOGGER = Logger.getLogger(LaMevaClasse.class.getName());
Aquesta sentència es pot afegir al començament d’una classe, per tal de fer-hi referència posteriorment. Per exemple:
LOGGER.log(Level.INFO, "un missatge");
El primer paràmetre és el nivell de la notificació, i el segon el missatge. El nivell pot variar entre SEVERE (valor més alt), WARNING, INFO, CONFIG, FINE, FINER i FINEST (valor més baix).
A més, aquest mètode permet utilitzar paràmetres per posició amb el codi {0}, {1}… o passant una excepció.
LOGGER.log(Level.FINE, "un missatge amb paràmetre {0}", "1");
LOGGER.log(Level.WARNING, "un missatge amb paràmetres {0} i {1}", new Object[]{"1", 2});
LOGGER.log(Level.SEVERE, "un missatge sense paràmetre i amb excepció", new RuntimeException("una excepció"));
Tot i que el mètode log(String) és el més estàndard i convenient, també hi ha mètodes de Logger per a cada nivell que faciliten l’escriptura:
severe(String), warning(String), info(String), config(String), fine(String), finer(String), finest(String)
Configuració
Si tenim un arxiu loggingConfigFile (tipus File) amb l’arxiu de configuració, hem de configurar el registre al començament de l’execució del programari mitjançant la classe LogManager:
LogManager.getLogManager().readConfiguration(new FileInputStream(loggingConfigFile));
Aquesta podria ser una configuració mínima amb nivell màxim de FINE que utilitza un formatador, SimpleFormatter per a la consola i un arxiu:
# two handlers defined:
handlers= java.util.logging.ConsoleHandler, java.util.logging.FileHandler
# default root level, overrided for package and for the LoggingTest class:
level = FINE
test.excepcions.level = INFO
test.excepcions.LoggingTest.level = FINEST
# console handler configuration (https://docs.oracle.com/en/java/javase/11/docs/api/java.logging/java/util/logging/ConsoleHandler.html)
java.util.logging.ConsoleHandler.level = FINEST
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
# default SimpleFormatter format
java.util.logging.SimpleFormatter.format=%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] %5$s %6$s%n
# file handler configuration (https://docs.oracle.com/en/java/javase/11/docs/api/java.logging/java/util/logging/FileHandler.html)
java.util.logging.FileHandler.level = ALL
java.util.logging.FileHandler.formatter=java.util.logging.SimpleFormatter
java.util.logging.FileHandler.pattern = %h/logging_test_%g.log
java.util.logging.FileHandler.limit = 1000000
java.util.logging.FileHandler.count = 10
El nivell FINE s’utilitza per defecte per tots els missatges. Es podria configurar per classe:
test.LoggingTest.level = FINEST
Interfícies gràfiques
Referències
- JavaFX 11 Javadoc
- GUI Architectures (Martin Fowler)
- The Observer Pattern Using Java 8
- Observer vs Pub-Sub pattern
- MVC Patterns
- Callbacks
Programació orientada a esdeveniments
Bucle d’esdeveniments
La programació d’interfícies d’usuari es fa mitjançant esdeveniments. Aquesta és la seqüència:
- L’usuari interactua amb el GUI
- Es produeix un esdeveniment
- En resposta, una peça de codi s’executa
- S’actualitza l’aparença del GUI
Aquestes operacions es produeixen dins del bucle d’esdeveniments (event loop). Els esdeveniments s’afegeixen a una mena de cua, i es van satisfent o gestionant amb el codi que el programador ha decidit. Aquest bucle és un sol fil, i per tant no es poden realitzar operacions massa llargues, ja que es bloquejaria el GUI i deixaria de respondre.
El codi equivalent seria:
do {
e = getNextEvent();
processEvent(e);
} while (e != quit);
El flux d’un programa amb GUI no està predeterminat: depèn dels esdeveniments que es produeixin. En contrast, les aplicacions que es recolzen en algorismes esperen unes dades d’entrada en un ordre i temps predeterminat.
Patró observador
El patró principal que s’utilitza a les interfícies gràfiques és el de l’observador. En aquest patró intervenen una parella subjecte/observador. El funcionament bàsic és que tenim un subjecte que genera esdeveniments i un o més observadors que els escolten. Això ens permet fer push dels esdeveniments, en lloc de fer polling. O sigui, comunicar-los quan passen, en lloc d’haver de comprovar si han ocorregut cada cert temps.
Un patró germà és el publish-subscribe, on parlem de missatges en lloc d’esdeveniments. Tenim publicadors que generen missatges, i els subscriptors interessats es registren i els reben. Aquest patró també es relaciona amb les cues de missatges, habitualment utilitzades conjuntament.
Implementació de les notificacions
Quan l’esdeveniment succeeix, el subjecte acaba notificant a tots els observadors amb una crida al un mètode anomenat update(…). Aquesta notificació o update pot implementar-se de diverses maneres:
- El mètode
update(...)dels observadors pot tenir diversos paràmetres per a indicar a l’observador quin esdeveniment s’ha produït. En el nostre exemple, un paràmetre amb l’esdeveniment anomenatEvent. - El mètode pot dir-se de moltes maneres. Per exemple,
onEvent()oactionPerformed()són altres nomenclatures habituals. - L’objecte
Eventpot contenir el subjecte, el tipus d’esdeveniment (si hi ha més d’un) i altres paràmetres addicionals d’ajuda per a l’observador. - Quan el subjecte genera diversos tipus d’esdeveniments, podem implementar-ho de diferents maneres:
- Tenir una sola classe
Eventi indicar el seu tipus en un camp amb, per exemple, unenum. - Implementar
Eventcom a una classe abstracta amb subclasses per a cada tipus d’esdeveniment, on cadascuna emmagatzema informació diversa. Això requerirà l’ús deinstanceofper a distinguir-los. - Tenir diverses signatures del mètode de notificació, una per tipus esdeveniment. Per exemple,
onEventX(),onEventY(), etc. L’avantatge és que cada mètode pot tenir paràmetres diferents per cada tipus d’esdeveniment.
- Tenir una sola classe
- Tots aquests mètodes no han de retornar res (tipus void). Només es vol notificar als observadors, però el subjecte no necessita res d’ells.
Callbacks
El patró observador utilitza múltiples noms per a la parella subjecte/observador. El subjecte també pot anomenar-se observable o event source. L’observador també pot anomenar-se handler, listener o callback. Tot depèn del context.
Els callbacks són el mateix concepte explicat diferent. Un callback és un codi que passem com a paràmetre a un component, i que aquest executarà més endavant, possiblement de forma asíncrona. Les callbacks s’utilitzen quan només hi ha un observador per al subjecte.
Per exemple, a JavaFX, un botó té aquest mètode:
void setOnAction(EventHandler<ActionEvent> value)
El botó és el subjecte i EventHandler és el callback que s’executarà quan es cliqui. Per la seva banda, EventHandler és una interfície funcional, és a dir, amb un sol mètode:
void handle(ActionEvent event)
El client (codi que utilitza la llibreria) haurà d’implementar el mètode handle, que rep ActionEvent, un objecte amb el source, target i type de l’esdeveniment.
JavaFX
Aquest apartat utilitzarà JavaFX per a la creació d’interfícies gràfiques. Aquesta plataforma substitueix Swing com a llibreria GUI de Java, i permet desenvolupar aplicacions d’escriptori.
Aquests són els aspectes principals dels components gràfics de JavaFX:
- Un programa JavaFX consisteix a una classe que estén la classe abstracta
javafx.application.Application. - El contenidor de màxim nivell màxim és
javafx.stage.Stage. Es correspon amb una finestra. - Els components visuals (nodes) estan continguts dins d’un scene:
javafx.scene.Scene. - Una aplicació pot contenir diverses scenes, però només una es pot mostrar al stage.
- Un scene conté un graf jeràrquic de nodes:
javafx.scene.Node.
Per tant, per construir la UI cal:
- Preparar un graf de scene.
- Construir un scene amb el node arrel del graf.
- Configurar el stage amb aquest scene.

Tipus de nodes
Els nodes poden ser de tres tipus:
- Arrel: el primer del graf. Pot ser del tipus 2.
- Parent (Branch)
- Group: és un node col.lectiu que renderitza tots els seus fills en ordre.
- Region: base per als controls UI, com Chart, Pane i Control.
- WebView: gestiona l’engine web.
- Leaf: no conté nodes fill.
Els nodes (components visuals) inclouen:
- Figures geomètriques (
javafx.scene.shape): Circle, Rectangle, Polygon, etc. - Controls (
javafx.scene.control): Button, Checkbox, Choice Box, Text Area, etc. - Contenidors (
javafx.scene.layout): Border Pane, Grid Pane, Flow Pane, etc. - Elements media (Audio, Video, Image)
Cada tipus de node té mètodes que permeten modificar el seu aspecte o el seu contingut, habitualment són getters i setters.
Per exemple: un Label és un node amb un text, i té dos mètodes per accedir i canviar el contingut: setText i getText.
També hi ha la possibilitat de treballar directament amb un canvas, dibuixant en ell. És la classe Canvas. Aquí hi ha una explicació de com funciona.
Aplicació
Ara veure’m un exemple mínim d’aplicació. Tenim els següents components gràfics:
- Stage: la finestra principal
- Scene: el contenidor del graf d’elements gràfics
- Graf scene: la jerarquia d’elements gràfics, en aquest exemple: Label.
public class JavaWorldApp extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
// creació del stage, scene i scene graph
primaryStage.setTitle("Hello world App");
Label label = new Label("Hello World!");
Scene scene = new Scene(label, 400, 200);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String args[]){
launch(args);
}
}
Scene Graph
Tenim bàsicament dos tipus de grafs de nodes: Group i Region.
Group
Group root = new Group();
ObservableList list = root.getChildren();
list.add(nodeObject1);
Scene scene = new Scene(root);
primaryStage.setScene(scene);
Region
StackPane pane = new StackPane();
ObservableList list = pane.getChildren();
list.add(nodeObject1);
Scene scene = new Scene(root);
primaryStage.setScene(scene);
Layouts

FXML
A l’hora de crear elements gràfics tenim dues opcions: crear-los programàticament o bé amb un arxiu de tipus XML anomenat FXML. El format FXML facilita el dibuix mitjançant eines de disseny com el Scene Builder. A més, permet associar el codi XML amb el codi Java:
- Cal definir un controlador, un objecte Java que serà el nexe de comunicació del món XML i el món Java. Aquest ha d’implementar la interfície javafx.fxml.Initializable.
- Defineix associacions entre objectes al FXML (propietat
fx:iddels elements) i objectes Java del controlador. - Defineix associacions entre accions al FXML (propietat
onActiondels elements) i mètodes Java del controlador.
Per a carregar un arxiu FXML cal fer les següents operacions:
FXMLLoader loader = new FXMLLoader();
loader.setController(controlador);
loader.setLocation(getClass().getResource("/cami/arxiu.fxml"));
Parent parent = loader.load();
Scene scene = new Scene(parent);
El camí pot ser absolut (utilitzant la jerarquia de paquets) o bé relatiu al paquet actual, sense utilitzar camí.
Les associacions al FXML es poden fer utilitzant un ID i amb una action:
<Label fx:id="inputLabel"> ... </Label>
...
<Button ... onAction="#onButtonClick" ... />
Aquest codi es correspondrà amb el següent al controlador:
@FXML
private Label inputLabel;
...
@FXML
private void onButtonClick(ActionEvent event) {
...
}
Amb aquest codi podem accedir a l’etiqueta definida al XML mitjançant l’objecte inputLabel, i cada cop que es cliqui al botó es cridarà al mètode onButtonClick.
Múltiples finestres
Una stage equival a una finestra.
Podem canviar el contingut d’una finestra modificant el graf de scenes. Això es pot fer amb el mètode:
scene.setRoot(Parent node)
Podem crear finestres modals de tres tipus:
- Alert
- TextInputDialog
- ChoiceDialog
El mètode start(Stage primaryStage) d’una aplicació permet establir la finestra principal, però es podrien crear noves, modals o no. Per fer-ho, crear una stage, i utilitzar els mètodes:
stage.initOwner(Window w)stage.initModality(Modality m)
Modality pot tenir tres valors:
Modality.NONE: un stage que no bloqueja cap altra finestra.Modality.WINDOW_MODAL: un stage que impedeix que els esdeveniments d’entrada es lliurin a totes les finestres des del seu pare fins a l’arrel. La seva arrel és la finestra més avantpassada sense owner.Modality.APPLICATION_MODAL: un stage que impedeix que els esdeveniments d’entrada es lliurin a totes les finestres des de la mateixa aplicació, excepte els de la seva jerarquia de fills.
Gestió d’esdeveniments
- Processament d’esdeveniments
- Classes anònimes i expressions Lambda
- Mètodes per afegir gestors i filtres
Els esdeveniments notifiquen a l’aplicació de les accions de l’usuari. Els esdeveniments són subclasses d’Event. Per exemple, MouseEvent, KeyEvent, DragEvent o WindowEvent.
Partim d’un exemple: un clic del ratolí a un botó. Llavors, un esdeveniment es compon de:
- Destí: el node on succeïx l’esdeveniment. Pot ser una finestra, una escena o un node. En l’exemple, el botó.
- Origen: el lloc on es genera l’esdeveniment. En l’exemple, el ratolí.
- Tipus: el tipus. En l’exemple, clicar el ratolí.

Processament d’esdeveniments
El processament de l’esdeveniment és el següent:
- Selecció del destí:
- Si és un esdeveniment de tecles (keys), l’element que tingui el focus.
- Si és un esdeveniment de mouse, l’element a sota. Si hi ha més d’un, el que estigui a sobre.
- Construcció de l’encaminament: en funció de la jerarquia dels nodes. És el camí des del stage fins arribar al node destí.
- Captura (camí des del stage fins al destí). Aquí no es criden els gestors, però sí els filtres, que poden consumir l’esdeveniment amb
event.consume()i finalitzar la captura. - Retorn (bombolla): pel camí de tornada cap al stage. Aquí es criden els gestors. Si el gestor d’un node no consumeix l’esdeveniment, un gestor del node pare pot fer-ho, permetent gestors comuns per diversos nodes fill.
Classes anònimes i expressions Lambda
Exemple de gestió d’un esdeveniment d’un botó (control de tipus Button):
button.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
System.out.println("Botó clicat!");
}
});
Aquest codi utilitza classes anònimes.
També podem utilitzar expressions Lambda, ja que els gestors d’esdeveniments són interfícies funcionals (un sol mètode abstracte):
buttn.setOnAction(
event -> System.out.println("Botó clicat!")
);
Mètodes per afegir gestors i filtres
Els filtres permeten gestionar el processament de l’esdeveniment i consumir-lo, si cal.
<T extends Event> void addEventFilter(
EventType<T> eventType, EventHandler<? super T> eventFilter)
<T extends Event> void removeEventFilter(
EventType<T> eventType, EventHandler<? super T> eventFilter)
Els gestors (handlers) permeten a les aplicacions prendre accions en funció del seu tipus, origen i destí.
<T extends Event> void addEventHandler(
EventType<T> eventType, EventHandler<? super T> eventHandler)
<T extends Event> void removeEventHandler(
EventType<T> eventType, EventHandler<? super T> eventHandler)
Per als gestors tenim els mètodes generals que hem vist i els mètodes de conveniència .setXXX() que faciliten escriure el codi sense haver d’indicar el tipus d’esdeveniment. Tots els setters treballen amb un sol handler, mentre que l’add/remove permet afegir diversos handlers al mateix esdeveniment.
button.addEventHandler(MouseEvent.MOUSE_CLICKED, mouseHandler);
button.setEventHandler(MouseEvent.MOUSE_CLICKED, mouseHandler);
button.setOnMouseClicked(mouseHandler);
button.setOnAction(actionHandler);
Aquests són alguns dels mètodes de conveniència disponibles:
- General:
setOnAction - Ratolí:
setOnMouseClicked, setOnMouseEntered, setOnMouseExited, setOnMousePressed - Teclat:
setOnKeyTyped, setOnKeyPressed, setOnKeyReleased
En general, setOnAction funciona per tots els controls. Hi ha casos especials, com per exemple si volem atendre el canvi de qualsevol contingut d’un TextField. Es pot utilitzar:
TextField.textProperty().addListener(ChangeListener listener)
I per escoltar un índex numèric sobre un ChoiceBox:
ChoiceBox.getSelectionModel().selectedIndexProperty().addListener(ChangeListener listener)
Pots veure la llista de controls i com utilitzar-los.
Patró de disseny UI
Un patró de disseny associat típicament al desenvolupament d’interfícies d’usuari (UI) és el model-vista-controlador (MVC). S’utilitza habitualment a l’entorn web, i la seva implementació pot variar. El següent diagrama és una opció.
Una particularització d’aquest patró és el model-vista-presentador (MVP), on el presentador és un controlador que fa d’intermediari entre la vista (passiva) i el model. Està més associat amb aplicacions natives com, per exemple, JavaFX.
Aquestes són les responsabilitats de cada component del patró:
- Vista: genera la part visual de l’aplicació. Envia esdeveniments cap al presentador, i rep peticions del presentador per actualitzar-se.
- Presentador: s’encarrega de mitjançar entre la vista i el model. No conté cap codi associat a la UI.
- Model: part del patró que s’encarrega d’accedir a funcionalitats o dades a una llibreria independent, que no té cap relació amb la presentació visual.

Encapsulació al patró MVP
En aquest esquema, és important encapsular correctament. Si es fa bé, tant la vista com el model serien substituïbles. Això només es pot aconseguir si les tres parts es relacionen mitjançant abstraccions d’un contracte, les quals poden ser implementades mitjançant una interfície Java.
Si fem bé l’encapsulació podem testar tant el presentador com el model. L’aproximació és utilitzar una vista passiva: la vista mai es comunica amb el model, i és el presentador qui gestiona els seus esdeveniments i l’actualitza. Per fer el testing general podem utilitzar un doble de proves de la vista.
Així es defineixen i relacionen les parts:
- La vista envia esdeveniments al seu únic observador, el presentador. Té mètodes que permeten al presentador actualitzar-la, i és agnòstic d’aquest. Només la vista conté classes de la llibreria visual (JavaFX).
- El presentador té una instància de la vista (per enviar-li actualitzacions de la part visual) i una instància del model (per enviar comandes o consultes).
- El model envia esdeveniments al presentador, el seu observador. El model és agnòstic respecte del funcionament del presentador i tampoc coneix cap aspecte visual.
A continuació es mostra una plantilla d’interfícies per a aquest patró.
interface View {
void setListener(ViewListener l); // permet registrar el presenter
// cal afegir comandes des del presentador
}
interface ViewListener {
// cal afegir mètodes que escolten esdeveniments de la view
}
interface Presenter extends ViewListener, ModelListener {
void start(); // mètode d'inici de l'aplicació
void stop(); // mètode de fi de l'aplicació
}
interface Model {
void setListener(ModelListener l); // permet registrar el presenter
// cal afegir els comandes i queries del presenter
}
interface ModelListener {
// cal afegir un mètode per cada esdeveniment generat pel model
}
Concurrència
Les aplicacions GUI (interfície gràfica d’usuari) de Java (inclosa JavaFX) són inherentment multifil. Diversos fils realitzen tasques diferents per mantenir la interfície d’usuari en sincronització amb les accions de l’usuari. JavaFX utilitza un únic fil, anomenat JavaFX Application Thread, per processar tots els esdeveniments de la interfície d’usuari. Els nodes que representen la interfície d’usuari d’una gràfica d’escena no són segurs. El disseny de nodes que no són segurs per a fils presenta avantatges i inconvenients. Són més ràpids, ja que no hi ha cap sincronització. L’inconvenient és que s’han d’accedir des d’un mateix fil per evitar estar en un estat il·legal. JavaFX posa una restricció a la qual s’ha d’accedir a un gràfic d’escena en directe des d’un únic fil, el fil d’aplicacions JavaFX. Aquesta restricció imposa indirectament una altra restricció que un esdeveniment d’UI no ha de processar una tasca de llarga durada, ja que farà que l’aplicació no respongui.

Si un altre fil vol modificar la GUI, cal que utilitzi la següent construcció per afegir la tasca a la cua d’esdeveniments:
Platform.runLater(new Runnable() {
@Override
public void run() {
// acció que es vol realitzar
}
});
Binding de propietats
Una propietat és un atribut accesible públicament i que afecta el seu estat i/o comportament. Les propietats són observables: poden notificar a observadors de canvis. Poden ser només lectura, només escriptura o lectura i escriptura.
El binding de les dades, en aquest context, es refereix a la relació entre variables d’un programa per tal de mantenir-se sincronitzades. A les GUI ens permet mantenir sincronitzats elements de la capa de model amb els elements GUI corresponents. Això s’aconsegueix gràcies a la implementació del patró observador.
Tipus de binding:
- Eager (ansiós) o lazy (mandrós): si el valor de variable es recalcula immediatament quan ho fa la dependència, o només quan es llegeix. Les propietats de JavaFX utilitzen avaluació mandrosa.
- Unidireccional o bidireccional: si només es fa en una direcció la sincronització, o en les dues.
Exemple de binding unidireccional:
IntegerProperty p1 = new SimpleIntegerProperty(1);
IntegerProperty p2 = new SimpleIntegerProperty(2);
p1.bind(p2); // p1 pren el valor de p2
p2.set(3);
int valor1 = p1.get(); // retorna 3
Exemple de binding bidireccional:
IntegerProperty p1 = new SimpleIntegerProperty(1);
IntegerProperty p2 = new SimpleIntegerProperty(2);
p1.bindBidirectional(p2);
p2.set(3);
int valor1 = p1.get(); // retorna 3
p1.set(4);
int valor2 = p2.get(); // retorna 4
Els binding es poden fer a JavaFX utilitzant les propietats associades als elements gràfics. Per exemple:
TextField tf1 = new TextField();
TextField tf2 = new TextField();
tf1.textProperty().bind(tf2.textProperty());
Per associar una etiqueta a una propietat del model de diferents tipus podem utilitzar el mètode asString():
Label l = new Label();
IntegerProperty p = new SimpleIntegerProperty(3);
l.textProperty().bind(p.asString());
Persistència POO
Fitxers en Java
Podem utilitzar fitxers en un format a mida. Aquests són alguns possibles casos d’ús.
Properties
Les Properties són un format pla per a configuració d’una aplicació en format text. S’utilitzen parelles clau/valor que poden llegir-se (load) i escriure’s (store).
JSON
Podem escriure un arxiu de text en format JSON, un format estructurat, i processar-lo utilitzant una de tantes llibreries existents. Per exemple, JSON-java. Poden servir per llegir i persistir l’estat d’una aplicació, quan la quantitat de dades no és massa gran.
YAML
YAML és un format d’entrada estructurat que també té algunes llibreries que permeten llegir-lo, com snakeyaml, però no està tan indicat per persistir.
Arxius d’accés aleatori
Podem utilitzar RandomAccessFile per a crear arxius binaris o de text amb registres de mida fixa. Això ens permet operar amb un índex sense haver de llegir tot l’arxiu com un stream. Hi ha utilitats a Java per exportar i importar tipus primitius utilitzant bytes, com ByteBuffer.
Serialització
La serialització i la deserialització són processos que permeten convertir un objecte (Java) en un format fàcil de persistir i a la inversa.
El mecanisme de la JRE es basa en l’utilització de ObjectInputStream i ObjectOutputStream. Permeten utilitzar un stream per llegir i escriure objectes. Cal que l’objecte implementi Serializable, i es pot fer serializació a mida implementat els mètodes privats writeObject i readObject.
Utilitzar els mecanismes Java de serializació pot fer que la nostra solució estigui tancada a altres llenguatges. Pot no ser un problema, si són arxius que no es compartiran fora.
JSON pot ser una solució de format més intercanviable. Les llibreries existents s’encarreguen de la serialització i deserialització.
Java Database Connectivity (JDBC)
- Controladors (drivers)
- Connexions (connections)
- Sentències (statements)
- Inserció i obtenció d’una clau generada
- java.sql.Date, java.sql.Time, and java.sql.Timestamp
- Excepcions
- Transaccions
- Pool de connexions
JDBC (Java DataBase Connectivity): API estàndard que permet llençar consultes a una BD relacional.
JDBC és el model de persistència bàsic a Java. En funció de la mida i el tipus de projecte és possible que necessitem ajuda per implementar aspectes recurrents al codi:
- Operacions CRUD: si necessitem fer CRUD per moltes taules, seria una feina molt feixuga.
- Generació de queries SQL: ens facilita tenir queries universals ben formades (dialectes).
- Gestió de transaccions: gestió per threads de transaccions i connexions a la BBDD.
- Control de concurrència (optimista, pessimista): mecanismes amb versions/timestamps o amb bloqueig de files.
Els paquets java.sql y javax.sql formen part de Java SE i contenen un bon nombre d’interfícies i algunes classes concretes, que conformen l’API de JDBC. Els components principals de JDBC són: els controladors, les connexions, les sentències i els resultats.
Controladors (drivers)
Un controlador JDBC és una col·lecció de classes Java que us permet connectar-vos a una determinada base de dades. Per exemple, MySQL té el seu propi controlador JDBC. Un controlador JDBC implementa moltes de les interfícies JDBC. Quan el codi utilitza un controlador JDBC determinat, en realitat només utilitza les interfícies estàndard JDBC. El controlador JDBC concret que s’utilitza s’amaga darrere de les interfícies JDBC. D’aquesta manera podeu connectar un nou controlador JDBC sense que el vostre codi ho noti.
Els drivers estan disponibles quan la llibreria (jar) corresponent està al classpath. Per exemple, per a MySQL 8.x podem comprovar-ho amb:
try {
Class.forName("com.mysql.cj.jdbc.Driver");
} catch (ClassNotFoundException e) {
System.out.println(name + "Falta llibreria");
}
Connexions (connections)
Una vegada carregat i inicialitzat un controlador JDBC, heu de connectar-vos a la base de dades. Es fa obtenint una connexió a la base de dades mitjançant l’API JDBC i el controlador carregat. Tota comunicació amb la base de dades es fa a través d’una connexió. Una aplicació pot tenir més d’una connexió oberta a una base de dades alhora, però cal estalviar connexions, ja que són cares, i tancar-les sempre.
Per obtenir una connexió, necessitem una URL, que és dependent de la BBDD concreta. Per exemple:
jdbc:mysql://localhost:3306/test
jdbc:sqlite:path/test.db
Podem obtenir una connexió:
Connection connection = DriverManager.getConnection(url, user, password);
// sentències ...
connection.close();
Una manera alternativa, i recomanable, d’obtenir una connexió és utilitzant el try-with-resources, ja que Connection és un AutoCloseable.
try (Connection connection = DriverManager.getConnection(url, user, password)) {
// sentències ...
} catch (SQLException e) {
e.printStackTrace();
}
Sentències (statements)
Una setència és el que utilitzeu per executar consultes i actualitzacions a la base de dades. Podeu utilitzar alguns tipus diferents d’enunciats. Cada declaració correspon a una sola consulta o actualització. Tenim bàsicament dos tipus de sentències en funció de si la sentència SQL té o no paràmetres (comodins amb ?).
Statement
try (Statement st = connection.createStatement()) {
int count = st.executeUpdate(sql1); // INSERT, UPDATE o DELETE
// o bé...
try (ResultSet rs = st.executeQuery(sql2)) { // SELECT
// processament...
}
}
PreparedStatement
try (PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setType1(1, valor1); // 1 a N, on Type pot ser Int, String...
ps.setType2(2, valor2);
int count = ps.executeUpdate(); // INSERT, UPDATE o DELETE
// o bé...
try (ResultSet rs = ps.executeQuery()) { // SELECT
// processament...
}
}
Conjunts de resultats (ResultSets)
Quan es realitza una consulta a la base de dades, s’obté un conjunt de resultats. A continuació, podeu recórrer aquest ResultSet per llegir el resultat de la consulta.
try (ResultSet rs = st.executeQuery(sql)) {
while (rs.next()) {
Type1 valor1 = rs.getType1(1);
Type2 valor2 = rs.getType2(2);
// o bé...
Type1 valor1 = rs.getType1("nom_columna1");
Type2 valor2 = rs.getType2("nom_columna2");
}
}
Inserció i obtenció d’una clau generada
De vegades es vol obtenir la clau que s’acaba de generar a una columna automàticament. Això es pot definir a MySQL amb AUTO_INCREMENT o bé a PostgreSQL amb SERIAL. Es pot aconseguir en dos pasos:
- utilitzant el paràmetre
Statement.RETURN_GENERATED_KEYSquan es crea elPreparedStatement. - Obtenint el
ResultSetmitjançant PreparedStatement.getGeneratedKeys(). Habitualment només hi haurà una columna, la posició 1.
int key;
String sql = "INSERT INTO taula (valor) VALUES (?)";
try (PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
ps.setString(1, valor);
ps.executeUpdate();
try (ResultSet rs = ps.getGeneratedKeys()) {
if (rs.next()) {
key = rs.getInt(1);
}
}
}
java.sql.Date, java.sql.Time, and java.sql.Timestamp
Aquestes classes estenen la funcionalitat d’altres Java per representar l’equivalent en SQL. Per exemple, java.sql.Date expressa el dia, mes i any. I java.sql.Time representa hora, minuts i segons. Finalment, java.sql.Timestamp representa java.util.Date fins als nanosegons.
Conversions entre java.sql.Date i java.util.Date: java.sql.Date estén (extends) java.util.Date. Per tant: tots els java.sql.Date són també java.util.Date.
En general, als nostres programes sempre es pot utilitzar java.util.Date, la data genèrica a Java.
Però a JDBC s’utilitza java.sql.Date en dos casos:
void PreparedStatement.setDate(int index, Date sdate). Per crear un java.sql.Date a partir d’un java.util.Date:- sdate = new java.sql.Date(udate.getTime()). Per exemple:
preparedStatement.setDate(3, new java.sql.Date(tasca.getDataInici().getTime()));
- sdate = java.sql.Date.valueOf(LocalDate.of(yyyy, mm, dd)).
- sdate = new java.sql.Date(udate.getTime()). Per exemple:
Date ResultSet.getDate(...)retorna unjava.sql.Date. Però no cal fer res especial: es pot assignar a un java.util.Date.- udate = sdate. Per exemple:
tasca.setDataInici(result.getDate("data_inici"));
- udate = sdate. Per exemple:
Excepcions
Les sentències SQL sobre JDBC poden generar excepcions SQLException.
Aquest tipus d’excepció té mètodes que poden ajudar a entendre el problema que s’ha produït:
getErrorCode(): codi específic del proveïdorgetSQLState(): codi SQLState stàndard
Les excepcions poden passar per diferents motius:
- Problemes de comunicació: connectivitat o servidor parat.
- Problemes d’autenticació: credencials incorrectes.
- Errors de sintaxi SQL, associats a la subclasse
SQLSyntaxErrorException. Són errors de programació. - Errors de violació de constraints, associats a la subclasse
SQLIntegrityConstraintViolationException. Indiquen que alguna constraint del DDL no es respectaria, com per exemple una Foreign Key, una Unique/Primary Key, etc. Aquest tipus es gestiona habitualment a l’aplicació per a detectar condicions recuperables.
Transaccions
Una transacció és un conjunt d’accions que s’han de dur a terme com una única acció atòmica. O totes es fan, o cap.
Quan es vol implementar una transacció, és important que totes les operacions de lectura i escriptura que es fan sobre la base de dades es facin compartint una única Connection. Per altra banda, no és correcte compartir una Connection entre diferents fils.
Inicieu una transacció per aquesta invocació:
connection.setAutoCommit(false);
Ara podeu continuar fent consultes i actualitzacions de taules. Totes aquestes accions formen part de la transacció.
Si alguna acció intentada dins de la transacció falla, haureu de desfer la transacció. Això es fa així:
connection.rollback();
Si totes les accions tenen èxit, hauríeu de confirmar la transacció. Un cop es fa, les accions són permanents a la base de dades i no hi ha marxa enrera. Es fa així:
connection.commit();
Exemple amb try/catch:
try (Connection conn = factory.getConnection()) {
conn.setAutoCommit(false);
try {
// sentències...
conn.commit();
} catch (Exception e) {
conn.rollback();
throw e; // excepcio original
}
} catch (SQLException e) {
throw new RuntimeException("error SQL", e);
}
En el cas que la mateixa Connection es vulgui utilitzar després sense transacció, caldria fer:
// no creem la connection
conn.setAutoCommit(false);
try {
// sentències...
conn.commit();
} catch (Exception e) {
conn.rollback();
throw e; // excepcio original
} finally {
conn.setAutoCommit(true);
}
Pool de connexions
El pool de connexions és un conegut patró d’accés a dades, que té com a objectiu principal reduir les despeses implicades en la realització de connexions de bases de dades i en operacions de bases de dades de lectura / escriptura.
En poques paraules, un pool de connexions és, al nivell més bàsic, una implementació de memòria cau de connexió de bases de dades, que es pot configurar per adaptar-se a requisits específics.
Es poden implementar adhoc, utilitzar llibreries de tercers o bé les de les implementacions del controlador JDBC. Sempre que sigui possible, és millor utilitzar un pool de connexions que fer-ho amb DriverManager.getConnection(…). Un pool ens crea un objecte javax.sql.DataSource, que permet obtenir connexions amb el seu mètode getConnection(). A més, el mètode close() de la connexió no la tanca, per poder reutilitzar-la un altre cop.
DataSource ds = // construir-lo segons la base de dades
Connection conn = ds.getConnection(); // obté una nova connexió
...
conn.close(); // retorna la connexió al pool (no la tanca)
Patrons de disseny
- Model de capes d’una aplicació
- Patrons del model
- Patrons de la base de dades relacional
- Patró DAO (Data Access Object)
- Estratègies DAO
- Control de concurrència
Model de capes d’una aplicació
Les capes d’una aplicació que necessita persistència podrien establir-se de la següent manera:
- Presentació: la part que s’encarrega de la interacció amb l’usuari. En el patró MVC, inclouria la vista i el controlador.
- Domini: lògica de negoci, relacionant les dades amb el seu comportament.
- Dades: comunicació amb altres sistemes que fan tasques necessàries per a la nostra aplicació. Per exemple, la base de dades.
On s’executa cada capa?
- Presentació: per a clients rics, al client. Per a B2C, al servidor (HTML).
- Domini: al servidor, més fàcil manteniment. Al client, si és desconnectat. Si cal dividir-la, cal aïllar les dues parts.
- Dades: al servidor, excepte si és un client desconnectat, llavors cal gestionar sincronitzacions.
Patrons del model
Dos estils d’implementació:
- Senzill: similar al disseny de DB, un objecte de domini per taula. Ús del patró Active record (objecte que embolica una fila d’una taula, encapsula l’accés i afegeix lògica de domini en aquestes dades).
- Ric: disseny diferent de la DB, amb herència, estratègies i altres patrons. Ús del patró Data Mapper (una capa de Mappers que mou les dades entre objectes i una base de dades mantenint-les independents les unes de les altres i del mateix mapper).
Patrons de la base de dades relacional
Cal fer un mapeig entre objectes i el món relacional perquè quan programem bases de dades relacionals, el seu model és diferent del dels objectes en memòria. Aquesta és una de les principals dificultats quan treballem amb els dos mons.
El patró general es diu Gateway: un objecte que encapsula l’accés a un recurs o sistema extern. Hi ha dues possibles implementacions:
- Row data gateway: una instància del gateway per cada registre que retorna una consulta.
- Table data gateway (o DAO): una instància gestiona tots els registres en una taula. Els registres es retornen a Record Sets.
Patró DAO (Data Access Object)
A continuació es pot veure el diagrama de classes del patró DAO i un exemple de seqüència.
Aquest patró utilitza un objecte de transferència, TransferObject, per intercanviar informació entre el client i la base de dades. Aquest objecte és una estructura de dades sense lògica de processament i habitualment immutable.
Cada objecte DAO realitza operacions sobre una taula: creació, lectura, modificació i esborrat.
Disseny d’un DAO
Aquests són els consells a l’hora de dissenyar un DAO:
- És convenient utilitzar sempre una interfície per definir un DAO, per tal d’aïllar millor el domini de les dades.
- Els mètodes públics, si utilitzen altres privats per a executar l’operació, han de compartir la mateixa
Connection. - No retornar mai objectes associats a la capa de base de dades, per exemple, evitant fer visible els
ResultSeto elsSQLException. - Si una excepció SQL no és recuperable, utilitzeu una
RuntimeExceptionembolcall d’aquesta. - No utilitzar camps al marge del
DataSource, ja que un DAO hauria de dissenyar-se sense estat per poder ser thread-safe.
Estratègies DAO
Objecte de transferència
L’objecte de transferència es pot utilitzar com es pot veure al següent exemple.
// suposem que a resultSet hi ha un registre d'una cerca
Persona persona = new Persona(); // transfer object
persona.setId(resultSet.getInt("id"));
persona.setName(resultSet.getString("name"));
...
return persona;
Els objectes de transferència poden implementar-se de diferents maneres, en particular, poden ser mutables o immutables. La preferència general seria que fossin immutables al client que els utilitza, malgrat que això pot significar complicar-los pel que fa a la seva programació.
- Com a l’exemple, amb getters i setters. La més convencional.
- Amb una interfície immutable, encara que la seva implementació sigui mutable. La més correcta.
- Amb camps públics i sense getters ni setters. La més senzilla.
- Alternativament, es pot utilitzar un Map<String, Object>, similar al concepte schemaless. No requereix classes, però es perd la validació en temps de compilació. Relacionat amb els JSON.
Col·lecció d’objectes de transferència
Com ja s’ha comentat, és millor no exposar objectes associats a la capa de dades al client. Per exemple, evitar fer visible els ResultSet. Així encapsulem i evitem dependències i haver de gestionar excepcions de tipus SQLException. En aquesta estratègia el DAO crea una sentència SQL i executa una consulta per obtenir un ResultSet. Llavors el DAO processa el ResultSet per recuperar tantes files de resultats coincidents com ho sol·liciti el client que fa la crida. Per a cada fila, el DAO crea un objecte de transferència i l’afegeix a una col·lecció que es retorna al client.
List<Persona> persones = new ArrayList<>();
while (resultSet.next()) {
Persona persona = new Persona(); // transfer object
persona.setId(resultSet.getInt("id"));
persona.setName(resultSet.getString("name"));
persones.add(to);
}
return persones;
Factoria de DAO
Quan tenim diversos DAO que cal crear per diferents taules, un patró habitual és la factoria de DAO.
public class DAOFactory {
private static DAOFactory instance;
private DAOFactory() {
// init ConnectionFactory
}
public static DAOFactory getInstance() {
if (instance == null)
instance = new DAOFactory();
return instance;
}
public CustomerDAO getCustomerDAO() {
// implementar-ho
}
public AccountDAO getAccountDAO() {
// implementar-ho
}
public OrderDAO getOrderDAO() {
// implementar-ho
}
}
Control de concurrència
Quan tractem de llegir i modificar dades, podríem afrontar alguns dilemes sobre la integritat i la validesa de la informació. Aquests dilemes sorgeixen a causa de les operacions de bases de dades xocant entre elles; per exemple, dues operacions d’escriptura o una operació de lectura i escriptura que col·lideixen.
Les estratègies per resoldre aquesta situació poden ser:
- Bloqueig pessimista: s’espera una col·lisió, i llavors es fa un bloqueig dels recursos implicats. Cap altre client pot accedir-los fins que no es lliurin.
- Bloqueig optimista: la col·lisió és poc probable. Es deixa fer, i quan acaba el processament, es comprova si hi ha hagut un problema.
- Bloqueig massa optimista: no s’esperen col·lisions. Potser és un sistema monousuari.
Les transaccions dels SGBD resolen aquests problemes gràcies a la implementació de les característiques ACID. Primer, totes les sentències individuals que s’executen són atòmiques, és a dir, o bé s’executen o no ho fan, però mai provoquen problemes de consistència. Segon, podem estendre aquesta capacitat per a un conjunt de sentències mitjançant l’ús de transaccions.
Les estratègies de resolució de col·lisions poden ser:
- Rendir-se
- Mostrar el problema, i deixar que decideixi l’usuari
- Barrejar els canvis
- Registrar el problema, i que ho resolgui algú altre més tard
- Ignorar la col·lisió
La implementació de transaccions assegura que no hi ha interferències entre elles. Tenim quatre nivells d’aïllament, que equilibren rendiment i l’exactitud de les dades, de menys a més restrictiu:
- Lectura no confirmada: les transaccions poden veure els canvis realitzats per altres transaccions fins i tot abans que aquests canvis es confirmin. Rarament utilitzat.
- Lectura confirmada: una transacció només veu els canvis realitzats per altres transaccions un cop aquests canvis s’han confirmat. Bon equilibri.
- Lectura repetible: assegura que si una transacció llegeix dades més d’una vegada, obtindrà el mateix resultat cada vegada, fins i tot si altres transaccions estan actualitzant les dades. No obstant això, no evita que altres transaccions afegeixin noves files (lectures fantasmes). Útil a aplicacions crítiques, com a les financeres.
- Serialitzable: el nivell d’aïllament més alt, on les transaccions estan completament aïllades entre elles, com si s’executessin una darrere l’altra en ordre. No importa el rendiment, només l’exactitud.
Tanmateix, les transaccions no sempre són la solució en l’àmbit de les aplicacions. Les lectures/escriptures de dades separades en el temps poden provocar contenció de dades. Per exemple, una transacció no pot esperar que un usuari modifiqui les dades després d’haver-les llegit en un formulari.
Els mecanismes del bloqueig pessimista s’implementen mitjançant ordres que permeten bloquejar registres de la base de dades. Es fa perquè hi ha alta contenció i el cost de bloquejar és menor que el de fer enrere una transacció. Els sistemes de BBDD implementen aquest mecanisme amb locks, que poden bloquejar registres o taules. Ho fan de dues formes, segons hi accedeixin lectors o escriptors:
- Locks compartits: múltiples lectors poden obtenir-los, i cap pot escriure mentre estigui algun actiu.
- Locks exclusius: només un escriptor pot modificar un registre.
En canvi, el bloqueig optimista s’implementa fent que hi hagi un error si hi ha hagut col·lisió, i llavors cal que l’usuari torni a fer l’operació. Pot implementar-se amb un control de timestamps, comptadors o versions d’un registre. La modificació dels registres actualitza aquests valors, i pot utilitzar-se com a condició per fer fallar una transacció. Aquest podria ser una possible transacció:
- Inici: guardar un timestamp/versió que marca l’inici de la transacció
- Fer canvis: llegir i intentar escriure a la base de dades
- Validar: veure si les dades modificades són les marcades inicialment
- Confirmar/Desfer: si no hi ha conflicte, fer els canvis; si hi ha, desfer-los
Píndoles Java
- Noms a Java
- Valors inicials de les variables
- Modificadors d’accés
- Expressions i conversió de tipus
- Emmagatzematge de variables
- Precedència d’operadors
- Local vs Instance vs Class variables
- Checked versus unchecked exceptions
- Interpretació de les excepcions
- Genèrics
- Optional
- Classes dins de classes
- Expressions Lambda
- Referències a mètodes
- Mètodes default i static a interfícies
- Programació funcional
- Modules
- Línia de comanda
- Referències
Noms a Java
CamelCase: pràctica d’escriure frases o paraules compostes eliminant espais i posant en majúscula la primera lletra de cada paraula.
Pot ser UpperCamelCase o lowerCamelCase.
Es recomanable evitar caràcters que no siguin lletres o números.
- Classes (i Interfaces): noms en UpperCamelCase
- Mètodes: verbs en lowerCamelCase
- Variables: en lowerCamelCase. Han de ser mnemònics i evitar variables d’una lletra, excepte per temporals: i,j,k,m,n (sencers) i c,d,e (caràcters)
- Constants: paraules en majúscules separades per subratllat (underscore)
- Paquets (packages): paraules en minúscules separades per punts. Solen tenir format de domini invertit (com.domain.subdomain). S’ha d’evitar el default package (sense nom)
Valors inicials de les variables
- Cada variable de classe, d’instància o component d’un array s’inicialitza amb un valor per defecte quan es crea:
- Per a byte, el valor per defecte és zero, o sigui, el valor de (byte)0.
- Per a short, el valor per defecte és zero, o sigui, el valor de (short)0.
- Per a int, el valor per defecte és zero, o sigui, 0.
- Per a long, el valor per defecte és zero, o sigui, 0L.
- Per a float, el valor per defecte és positive zero, o sigui, 0.0f.
- Per a double, el valor per defecte és positive zero, o sigui, 0.0d.
- Per a char, el valor per defecte és el null character, o sigui, ‘\u0000’.
- Per a boolean, el valor per defecte és false.
- Per a tots els tipus referència, el valor per defecte és null.
- Cada paràmetre d’un mètode o constructor s’inicialitza amb el valor de l’argument corresponent quan s’invoca el mètode o constructor.
- El paràmetre d’una excepció s’inicialitza amb l’objecte que representa l’excepció.
- Una variable local ha de ser inicialitzada explícitament abans de ser utilitzada, amb una inicialització o assignació.
Modificadors d’accés
Els modificadors d’accés indiquen el nivell d’accés per a variables, mètodes i constructors. En tenim quatre, de menys a més restrictius:
- public: sense restriccions, totes les classes de tots els paquets poden accedir.
- protected: només podem accedir des del mateix paquet o des de qualsevol subclasse, encara que no estigui al mateix paquet.
- default (sense paraula clau): només podem accedir des del mateix paquet.
- private: només es pot accedir des de la mateixa classe.
Expressions i conversió de tipus
Ordre de les expressions
A Java, les expressions s’avaluen d’esquerra a dreta.
Java no executarà parts de l’expressió quan no sigui necessari. En particular:
- Si tenim expressions AND i el resultat d’una a l’esquerra és
false, ja no es continua avaluant a la dreta. - Si tenim expressions OR i el resultat d’una a l’esquerra és
true, ja no es continua avaluant a la dreta.
Tipus primitius
Quan diversos tipus intervenen en una expressió, tots es converteixen al mateix tipus mitjançant unes normes de promoció. Per començar, tots els char / byte / short es promouen a int. Si hi ha algun long, a long. Si hi ha algun float, a float. I si hi ha algun double, a double.
La conversió només es pot fer amb tipus numèrics (exclou el boolean). Hi ha de dos tipus:
- Widening: cap a un tipus més ample, no cal utilitzar cap notació.
- Narrowing: cap a un tipus més estret, cal utilitzar el cast: ( tipus ). La conversió pot perdre informació.
Objectes
Tenim dos tipus:
- Upcasting: quan volem passar el tipus d’un objecte des de la subclasse a una superclasse.
- Downcasting: quan volem passar el tipus d’un objecte des d’una superclasse a una subclasse.
L’operació de downcasting sol venir precedida de l’ús de l’operador instanceof. Aquest operador retorna true si la classe és del tipus que es pregunta, si és una subclasse o si implementa la interfície.
Autoboxing i unboxing
El boxing / unboxing permet convertir automàticament entre els tipus primitius i les classes embolcall (boolean/Boolean, byte/Byte, char/Character, float/Float, int/Integer, long/Long, short/Short i double/Double):
- Boxing: conversió automàtica que es produeix des d’un tipus primitiu cap a un objecte.
- Unboxing: conversió automàtica que es produeix des d’un objecte cap a un tipus primitiu.
Character ch = 'a'; // autoboxing
int val = new Integer(-8); // unboxing
Emmagatzematge de variables
Java passa els paràmetres d’un mètode per valor. Però el valor d’un objecte és una referència. Això també inclou qualsevol array (de primitius o objectes).
Per tant, mai podem modificar el valor d’una variable primitiva, ni el d’un objecte (la referència). El que es pot fer és, si l’objecte és mutable, modificar-lo.
Precedència d’operadors
| level | Operator | Description | Associativity |
|---|---|---|---|
| 16 | [] . () | access array element access object member parentheses | left to right |
| 15 | ++ – | unary post-increment unary post-decrement | not associative |
| 14 | ++ – + - ! ~ | unary pre-increment unary pre-decrement unary plus unary minus unary logical NOT unary bitwise NOT | right to left |
| 13 | () new | cast object creation | right to left |
| 12 | * / % | multiplicative | left to right |
| 11 | + - + | additive string concatenation | left to right |
| 10 | << >> >>> | shift | left to right |
| 9 | < <= > >= instanceof | relational | not associative |
| 8 | == != | equality | left to right |
| 7 | & | bitwise AND | left to right |
| 6 | ^ | bitwise XOR | left to right |
| 5 | | | bitwise OR | left to right |
| 4 | && | logical AND | left to right |
| 3 | || | logical OR | left to right |
| 2 | ?: | ternary | right to left |
| 1 | = += -= *= /= %= &= ^= |= <<= >>= >>>= | assignment | right to left |
Local vs Instance vs Class variables
| characteristic | Local variable | Instance variable | Class variable |
|---|---|---|---|
| Where declared | In a method, constructor, or block. | In a class, but outside a method. Typically private. | In a class, but outside a method. Must be declared static. Typically also final. |
| Use | Local variables hold values used in computations in a method. | Instance variables hold values that must be referenced by more than one method (for example, components that hold values like text fields, variables that control drawing, etc), or that are essential parts of an object’s state that must exist from one method invocation to another. | Class variables are mostly used for constants, variables that never change from their initial value. |
| Lifetime | Created when method or constructor is entered. Destroyed on exit. | Created when instance of class is created with new. Destroyed when there are no more references to enclosing object (made available for garbage collection). | Created when the program starts. Destroyed when the program stops. |
| Scope/Visibility | Local variables (including formal parameters) are visible only in the method, constructor, or block in which they are declared. Access modifiers (private, public, …) can not be used with local variables. All local variables are effectively private to the block in which they are declared. No part of the program outside of the method / block can see them. A special case is that local variables declared in the initializer part of a for statement have a scope of the for statement. | Instance (field) variables can been seen by all methods in the class. Which other classes can see them is determined by their declared access: private should be your default choice in declaring them. No other class can see private instance variables. This is regarded as the best choice. Define getter and setter methods if the value has to be gotten or set from outside so that data consistency can be enforced, and to preserve internal representation flexibility. Default (also called package visibility) allows a variable to be seen by any class in the same package. private is preferable. public. Can be seen from any class. Generally a bad idea. protected variables are only visible from any descendant classes. Uncommon, and probably a bad choice. | Same as instance variable, but are often declared public to make constants available to users of the class. |
| Declaration | Declare before use anywhere in a method or block. | Declare anywhere at class level (before or after use). | Declare anywhere at class level with static. |
| Initial value | None. Must be assigned a value before the first use. | Zero for numbers, false for booleans, or null for object references. May be assigned value at declaration or in constructor. | Same as instance variable, and it addition can be assigned value in special static initializer block. |
| Access from outside | Impossible. Local variable names are known only within the method. | Instance variables should be declared private to promote information hiding, so should not be accessed from outside a class. However, in the few cases where there are accessed from outside the class, they must be qualified by an object (eg, myPoint.x). | Class variables are qualified with the class name (eg, Color.BLUE). They can also be qualified with an object, but this is a deceptive style. |
| Name syntax | Standard rules. | Standard rules, but are often prefixed to clarify difference from local variables, eg with my, m, or m_ (for member) as in myLength, or this as in this.length. | static public final variables (constants) are all uppercase, otherwise normal naming conventions. Alternatively prefix the variable with “c_” (for class) or something similar. |
Checked versus unchecked exceptions
Unchecked exceptions:
- represent defects in the program (bugs) - often invalid arguments passed to a non-private method. To quote from The Java Programming Language, by Gosling, Arnold, and Holmes: “Unchecked runtime exceptions represent conditions that, generally speaking, reflect errors in your program’s logic and cannot be reasonably recovered from at run time.”
- are subclasses of
RuntimeException, and are usually implemented usingIllegalArgumentException,NullPointerException, orIllegalStateException - a method is not obliged to establish a policy for the unchecked exceptions thrown by its implementation (and they almost always do not do so)
Checked exceptions:
- represent invalid conditions in areas outside the immediate control of the program (invalid user input, database problems, network outages, absent files)
- are subclasses of
Exception - a method is obliged to establish a policy for all checked exceptions thrown by its implementation (either pass the checked exception further up the stack, or handle it somehow)
Interpretació de les excepcions
Hem vist que podem tenir excepcions Checked (subclasses de Exception) i Unchecked (subclasses de RuntimeException).
Si mirem la jerarquia de classes, totes extenen Throwable, la classe pare de totes:
Els tres mètodes més importants de Throwable són:
public String getMessage(): conté el missatge d’excepció.public StackTraceElement[] getStackTrace(): conté la traça de la pila de l’excepció.public Throwable getCause(): opcionalment, conté una referència a l’excepció que ha causat aquesta. Pot ser una cadena de diverses excepcions.
Stacktrace
Un stacktrace no ha de ser necessàriament un error. Es pot obtenir el valor actual mitjançant aquest codi:
public class TestStackTrace {
public static void one() {
two();
}
public static void two() {
three();
}
public static void three() {
StackTraceElement[] trace = Thread.currentThread().getStackTrace();
for (StackTraceElement elem : trace) {
System.out.println(elem);
}
}
public static void main(String[] args) {
one();
}
}
Cause
Si pots, guarda la causa quan facis throw d’una excepció més específica.
try {
doSomething();
} catch (NumberFormatException e) {
throw new MyBusinessException(e, ErrorCode.CONFIGURATION_ERROR);
} catch (IllegalArgumentException e) {
throw new MyBusinessException(e, ErrorCode.UNEXPECTED);
}
Estructura típica
paquet.NomDeLaException: missatge que explica la excepció al getMessage()
at paquet.Classe.metode(Classe.java:XXX)
at paquet.Classe.metode(Classe.java:XXX)
...
Caused by: paquet.NomDeLaException: missatge que explica la excepció al getMessage()
at paquet.Classe.metode(Classe.java:XXX)
at paquet.Classe.metode(Classe.java:XXX)
...
Exemple
A continuació veiem una excepció amb les causes encadenades (Caused by). Els punts suspensius expressen la repetició de les línies respecte de l’excepció pare.
org.hibernate.service.spi.ServiceException: Unable to create requested service [org.hibernate.engine.jdbc.env.spi.JdbcEnvironment]
at org.hibernate.service.internal.AbstractServiceRegistryImpl.createService(AbstractServiceRegistryImpl.java:275)
at org.hibernate.service.internal.AbstractServiceRegistryImpl.initializeService(AbstractServiceRegistryImpl.java:237)
at org.hibernate.service.internal.AbstractServiceRegistryImpl.getService(AbstractServiceRegistryImpl.java:214)
at org.hibernate.id.factory.internal.DefaultIdentifierGeneratorFactory.injectServices(DefaultIdentifierGeneratorFactory.java:152)
at org.hibernate.service.internal.AbstractServiceRegistryImpl.injectDependencies(AbstractServiceRegistryImpl.java:286)
at org.hibernate.service.internal.AbstractServiceRegistryImpl.initializeService(AbstractServiceRegistryImpl.java:243)
at org.hibernate.service.internal.AbstractServiceRegistryImpl.getService(AbstractServiceRegistryImpl.java:214)
at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.<init>(InFlightMetadataCollectorImpl.java:179)
at org.hibernate.boot.model.process.spi.MetadataBuildingProcess.complete(MetadataBuildingProcess.java:119)
at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.metadata(EntityManagerFactoryBuilderImpl.java:904)
at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:935)
at org.hibernate.jpa.HibernatePersistenceProvider.createEntityManagerFactory(HibernatePersistenceProvider.java:56)
at javax.persistence.Persistence.createEntityManagerFactory(Persistence.java:79)
at javax.persistence.Persistence.createEntityManagerFactory(Persistence.java:54)
at cotxes.CotxesDAO.getEMF(CotxesDAO.java:205)
at cotxes.CotxesDAO.access$400(CotxesDAO.java:15)
at cotxes.CotxesDAO$JPATransaction.result(CotxesDAO.java:223)
at cotxes.CotxesDAO.findMarques(CotxesDAO.java:81)
at cotxes.ProvaCotxes.processCommand(ProvaCotxes.java:69)
at cotxes.ProvaCotxes.prova(ProvaCotxes.java:49)
at cotxes.ProvaCotxes.main(ProvaCotxes.java:18)
Caused by: org.hibernate.exception.JDBCConnectionException: Error calling DriverManager#getConnection
at org.hibernate.exception.internal.SQLStateConversionDelegate.convert(SQLStateConversionDelegate.java:115)
at org.hibernate.exception.internal.StandardSQLExceptionConverter.convert(StandardSQLExceptionConverter.java:42)
at org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(SqlExceptionHelper.java:113)
at org.hibernate.engine.jdbc.connections.internal.BasicConnectionCreator.convertSqlException(BasicConnectionCreator.java:118)
at org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionCreator.makeConnection(DriverManagerConnectionCreator.java:37)
at org.hibernate.engine.jdbc.connections.internal.BasicConnectionCreator.createConnection(BasicConnectionCreator.java:58)
at org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl$PooledConnections.addConnections(DriverManagerConnectionProviderImpl.java:363)
at org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl$PooledConnections.<init>(DriverManagerConnectionProviderImpl.java:282)
at org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl$PooledConnections.<init>(DriverManagerConnectionProviderImpl.java:260)
at org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl$PooledConnections$Builder.build(DriverManagerConnectionProviderImpl.java:401)
at org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl.buildPool(DriverManagerConnectionProviderImpl.java:112)
at org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl.configure(DriverManagerConnectionProviderImpl.java:75)
at org.hibernate.boot.registry.internal.StandardServiceRegistryImpl.configureService(StandardServiceRegistryImpl.java:100)
at org.hibernate.service.internal.AbstractServiceRegistryImpl.initializeService(AbstractServiceRegistryImpl.java:246)
at org.hibernate.service.internal.AbstractServiceRegistryImpl.getService(AbstractServiceRegistryImpl.java:214)
at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.buildJdbcConnectionAccess(JdbcEnvironmentInitiator.java:145)
at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.initiateService(JdbcEnvironmentInitiator.java:66)
at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.initiateService(JdbcEnvironmentInitiator.java:35)
at org.hibernate.boot.registry.internal.StandardServiceRegistryImpl.initiateService(StandardServiceRegistryImpl.java:94)
at org.hibernate.service.internal.AbstractServiceRegistryImpl.createService(AbstractServiceRegistryImpl.java:263)
... 20 more
Caused by: com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Communications link failure
The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server.
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:408)
at com.mysql.jdbc.Util.handleNewInstance(Util.java:425)
at com.mysql.jdbc.SQLError.createCommunicationsException(SQLError.java:990)
at com.mysql.jdbc.MysqlIO.<init>(MysqlIO.java:342)
at com.mysql.jdbc.ConnectionImpl.coreConnect(ConnectionImpl.java:2197)
at com.mysql.jdbc.ConnectionImpl.connectOneTryOnly(ConnectionImpl.java:2230)
at com.mysql.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:2025)
at com.mysql.jdbc.ConnectionImpl.<init>(ConnectionImpl.java:778)
at com.mysql.jdbc.JDBC4Connection.<init>(JDBC4Connection.java:47)
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:408)
at com.mysql.jdbc.Util.handleNewInstance(Util.java:425)
at com.mysql.jdbc.ConnectionImpl.getInstance(ConnectionImpl.java:386)
at com.mysql.jdbc.NonRegisteringDriver.connect(NonRegisteringDriver.java:330)
at java.sql.DriverManager.getConnection(DriverManager.java:664)
at java.sql.DriverManager.getConnection(DriverManager.java:208)
at org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionCreator.makeConnection(DriverManagerConnectionCreator.java:34)
... 35 more
Caused by: java.net.ConnectException: Connection refused
at java.net.PlainSocketImpl.socketConnect(Native Method)
at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:345)
at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206)
at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188)
at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392)
at java.net.Socket.connect(Socket.java:589)
at com.mysql.jdbc.StandardSocketFactory.connect(StandardSocketFactory.java:211)
at com.mysql.jdbc.MysqlIO.<init>(MysqlIO.java:301)
... 50 more
Genèrics
Mètodes genèrics
- Totes les declaracions genèriques del mètode tenen una secció de paràmetre tipus delimitada per claudàtors d’angle que precedeix el tipus de retorn del mètode (<E> en el següent exemple).
- Cada secció de paràmetres de tipus conté un o més paràmetres de tipus separats per comes. Un paràmetre tipus, també conegut com a variable de tipus, és un identificador que especifica un nom de tipus genèric.
- Els paràmetres de tipus han de ser una lletra majúscula. Convencions: ‘T’ per tipus, ‘E’ per a elements de col·leccions, ‘S’ per a serveis i K i V per a claus i valors de mapes.
- Els paràmetres de tipus es poden utilitzar per declarar el tipus de devolució i actuar com a marcadors per als tipus d’arguments passats al mètode genèric, que es coneixen com a arguments de tipus reals.
- El cos d’un mètode genèric es declara com el de qualsevol altre mètode. Tingueu en compte que els paràmetres de tipus només poden representar tipus de referència, no tipus primitius (com int, double i char).
public static <E> void printArray( E[] inputArray ) {
// Display array elements
for(E element : inputArray) {
System.out.printf("%s ", element);
}
System.out.println();
}
Paràmetres de tipus delimitat
Hi pot haver moments en què voldreu restringir els tipus de tipus que es permeten passar a un tipus de paràmetre. Per exemple, un mètode que opera sobre números només pot voler acceptar instàncies de Number o de les seves subclasses. Per a què serveixen els paràmetres del tipus de límit.
Per declarar un paràmetre de tipus delimitat, enumereu el nom del paràmetre del tipus, seguit de la paraula clau extends, seguit tipus delimitant.
public static <T extends Comparable<T>> T maximum(T x, T y, T z) {
T max = x; // assume x is initially the largest
if(y.compareTo(max) > 0) {
max = y; // y is the largest so far
}
if(z.compareTo(max) > 0) {
max = z; // z is the largest now
}
return max; // returns the largest object
}
Classes genèriques
Una declaració de classe genèrica sembla una declaració de classe no genèrica, tret que el nom de classe sigui seguit per una secció de paràmetre tipus.
Com en els mètodes genèrics, la secció de paràmetres de tipus d’una classe genèrica pot tenir un o més paràmetres de tipus separats per comes. Aquestes classes es coneixen com a classes parametrizades o tipus parametrizats perquè accepten un o més paràmetres.
public class Box<T> {
private T t;
public void add(T t) {
this.t = t;
}
public T get() {
return t;
}
}
Interfícies genèriques
Imaginem que tenim aquesta interfície:
interface Container<T> {
T get();
}
La podríem implementar amb una classe genèrica:
public class GenericContainer<T> implements Container<T> {
private T t;
public GenericContainer(T t) {
this.t = t;
}
@Override
public T get() {
return t;
}
}
Aquesta implementació serveix per qualsevol tipus de paràmetre. Però també podem fer-ho concretant el tipus i esborrant el paràmetre:
public class StringContainer implements Container<String> {
private String s;
public StringContainer(String s) {
this.s = s;
}
@Override
public String get() {
return s;
}
}
Aquesta implementació serveix només per a Strings, però les dues implementen Container.
Optional
La classe Optional<T> és un contenidor que pot o no tenir un valor null. Si el valor està present, isPresent() retorna true i get() retorna el valor.
Ens permet evitar haver de comprovar object != null al codi contínuament, reduint el nombre de NullPointerException i construint APIs més expressives. Per exemple: no cal explicar si un mètode retorna o no null, si el que retorna és un Optional<T>.
S’ha d’utilitzar sempre com a valor de retorn d’un mètode. Evitar sempre que sigui un camp de l’objecte o un paràmetre d’un constructor o mètode.
public Optional<String> findName() { // API expressiva
String name = findNameInDatabase();
return Optional.ofNullable(name);
}
Optional<String> optional = findName();
if (optional.isPresent()) {
System.out.println("trobat " + optional.get());
}
else {
System.out.println("no trobat!);
}
Classes dins de classes
Java permet definir classes dins de classes. Això permet agrupar i encapsular, fent més llegible i gestionable el codi.
Classes estàtiques imbricades
És una classe relacionada amb la classe exterior, però que no pot fer referència a les variables d’instància d’aquesta. De fet, és com una classe normal que s’ha imbricat dins d’una altra, per qüestions de conveniència.
class OuterClass {
...
static class StaticNestedClass {
...
}
}
Classes imbricades
Les classes imbricades (no estàtiques) estan associades amb una instància de la classe que les conté.
class OuterClass {
...
class InnerClass {
...
}
}
Si una declaració utilitza una variable o paràmetre amb el mateix nom que una altra a un àmbit que el conté, llavors la declaració encobreix aquesta, i podem accedir-hi amb la sintaxi OuterClass.this.variable.
Classes locals
Una classe es pot definir localment, en qualsevol bloc de codi. Aquestes classes només poden accedir variables locals que siguin finals o efectivament finals: el seu valor no canvia després d’una declaració amb inicialització.
public class LocalClassExample {
...
public static void metode() {
...
class LocalClass {
...
}
}
}
Classes anònimes
Les classes anònimes permeten declarar i inicialitzar una classe al mateix temps. Són com classes locals, però sense nom. S’utilitzen quan només cal utilitzar una classe un cop. Es consideren expressions.
Es componen de:
- L’operador new.
- El nom d’una interfície a implementar o una classe a estendre.
- Els parèntesis amb els arguments d’un constructor.
- Un bloc de codi de la classe.
...
Runnable tasca = new Runnable() {
@Override
public void run() {
...
}
};
...
Les classes anònimes no poden contenir declaracions de constructors, ni poden accedir a variables locals que no siguin finals o efectivament finals.
Expressions Lambda
Una expressió lambda és una forma d’instanciar una interfície funcional. Una interfície funcional és aquella que només té un mètode abstracte.
Per exemple, partim d’una interfície funcional i un mètode que la utilitza:
@FunctionalInterface
interface MyFunctionalInterface {
String myOnlyMethod(String input);
}
static String process(String input, MyFunctionalInterface mfi) {
return mfi.myOnlyMethod(input);
}
Una expressió lambda permet definir i instanciar una classe que implementa la interfície funcional només escrivint el codi que falta: el seu únic mètode.
Com podem cridar aquest mètode? Sense i amb expressió lambda:
String output = process("input", new MyFunctionalInterface(){
@Override
public String myOnlyMethod(String input) {
return input.toUpperCase();
}
});
// versió lambda
String output = process("input", (input) -> input.toUpperCase());
Les expressions lambda són molt habituals al disseny d’interfícies gràfiques. Exemple de gestió d’un esdeveniment d’un botó (control de tipus Button):
button.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
System.out.println("Botó clicat!");
}
});
Aquest codi utilitza classes anònimes.
També podem utilitzar expressions Lambda, ja que els gestors d’esdeveniments són interfícies funcionals (un sol mètode abstracte):
button.setOnAction(
event -> System.out.println("Botó clicat!")
);
Veure https://docs.oracle.com/javase/tutorial/java/javaOO/nested.html
Referències a mètodes
De vegades, una expressió lambda només crida a un mètode. Llavors, és més fàcil i clar fer una referència al mètode. S’utilitza l’operador doble dos punts (double colon) amb el format Objecte::metode.
Tenim quatre tipus de referències. Veiem exemples a partir d’aquest codi:
class Persona {
private String name;
private int age;
Persona(String name, int age) {
this.name = name;
this.age = age;
}
String getName() {
return name;
}
Integer getAge() {
return age;
}
public int compareByName(Persona p) {
return getName().compareTo(p.getName());
}
public int compareByAge(Persona p) {
return getAge().compareTo(p.getAge());
}
@Override
public String toString() {
return name + ":" + age;
}
}
@FunctionalInterface
interface CreadorPersones {
Persona create(String name, int age);
}
class ProveidorComparadors {
public int compareByName(Persona a, Persona b) {
return a.getName().compareTo(b.getName());
}
public int compareByAge(Persona a, Persona b) {
return a.getAge().compareTo(b.getAge());
}
}
Aquí es veuen els quatre tipus de referències:
// 4: Referència a constructor
CreadorPersones creator; // mètode amb els mateixos paràmetres que el constructor
creator = (name, age) -> new Persona(name, age); // lambda
creator = Persona::new; // ref.mètode
// 1: Referència a mètode estàtic
List<Persona> list = Arrays.asList(createArray(creator));
list.forEach((person) -> System.out.println(person)); // lambda
list.forEach(System.out::println); // ref.mètode
// 2: Referència a mètode d'instància d'un objecte particular
ProveidorComparadors provComparadors = new ProveidorComparadors();
Arrays.sort(createArray(creator), (a, b) -> provComparadors.compareByName(a, b)); // lambda
Arrays.sort(createArray(creator), provComparadors::compareByName); // ref.mètode
// 3: Referència a mètode d'instància d'objecte arbitrari d'un tipus particular
Arrays.sort(createArray(creator), (a, b) -> a.compareByName(b)); // lambda
Arrays.sort(createArray(creator), Persona::compareByName); // ref.mètode
Mètodes default i static a interfícies
Abans de Java 8, les interfícies només podien tenir mètodes abstractes. Java 8 introdueix el concepte de mètodes default que permeten a les interfícies tenir mètodes amb implementació sense afectar les classes que implementen la interfície.
Per definir-los, només cal utilitzar el keyword “default” davant de la signatura.
També s’afegeixen els mètodes static, que permeten agrupar mètodes d’ajuda d’una classe.
public interface Thermometer {
double getCelsius();
default double getFahrenheit() {
return convertToFahrenheit(getCelsius());
}
static double convertToFahrenheit(double celsius) {
return celsius * 1.8 + 32;
}
}
Programació funcional
Una interfície funcional en Java és una que conté un sol mètode abstracte per a implementar. Pot contenir mètodes default i static implementats.
public interface MyFunctionalInterface {
public void execute();
}
També podria contenir mètodes default i static:
public interface MyFunctionalInterface2{
public void execute();
public default void print(String text) {
System.out.println(text);
}
public static void print(String text, PrintWriter writer) throws IOException {
writer.write(text);
}
}
Una interfície funcional es pot implementar amb una expressió lambda:
MyFunctionalInterface lambda = () -> {
System.out.println("Executing...");
}
Java té una sèrie d’interfícies funcionals a la seva llibreria.
Function
public interface Function<T,R> {
public <R> apply(T parameter);
}
Consumer
public interface Consumer<T> {
void accept(T t);
}
Predicate
public interface Predicate {
boolean test(T t);
}
Supplier
public interface Supplier<T> {
T get();
}
Modules
Un mòdul de Java (9+) és un conjunt de paquets reutilitzables. Es defineix utilitzant un arxiu anomenat modules-info.java a l’arrel del codi. El format base és:
module modulename {
// directives
}
El nom del mòdul pot utilitzar punts, com els paquets. Les directives permeten definir:
- Les seves dependències:
requires modulename. A més, si s’afegeix la paraula clautransitiveal mig, indica que tercers mòduls que el requereixin també tindran aquestes dependències implícitament. - Quins paquets estaran disponibles a altres mòduls:
exports packagename. - Quins serveis ofereix:
provides interfacename with classname. - Quins serveix consumeix:
uses interfacename. - Quins paquets estaran disponibles a altres mòduls mitjançant reflection:
opens packagename [to modulename, modulename...].
Regla important a respectar: un paquet només ha d’aparèixer en un mòdul.
A continuació es mostren els module-info.java de tres mòduls que permeten definir una API (my.interface), implementar-la (my.provider) i utilitzar-la (my.consumer).
module my.provider {
requires my.interface;
provides app.api.MyService with app.impl.MyServiceImpl; // ofereix el servei
}
module my.consumer {
requires my.interface;
uses app.api.MyService; // consumeix el servei
}
module my.interface {
exports app.api; // per al consumer i el provider
}
El consumer pot buscar totes les implementacions i instanciar-les. En aquest cas, només trobaria una, la del provider. Però podria haver més mòduls amb més implementacions del mateix servei.
ServiceLoader<MyService> loader = ServiceLoader.load(MyService.class);
for (MyService service: loader) {
// ...
}
Un servei només s’instancia un cop per cada ServiceLoader.
Línia de comanda
Imaginem que tenim una classe test.HelloModular amb un main que volem executar, i aquesta depèn d’un jar, stleary-json.jar, que no és modular.
Si volem comprovar si un jar és modular, tenim dues eines:
$ jar -tf nom-de-larxiu.jar # llista el contingut
$ jar --file=nom-de-larxiu.jar --describe-module # descriu la modularitat
Si no volem utilitzar mòduls en la nostra aplicació, podem compilar i executar així:
$ javac -cp lib/stleary-json.jar -d classes `find src -name *.java`
$ java -cp classes:lib/stleary-json.jar test.HelloModular
Si en canvi volem utilitzar mòduls, necessitem crear un module-info.java com aquest:
module hello_modular {
requires org.json; // nom que hi ha a Automatic-Module-Name del jar
}
stlearly-json.jar és un mòdul automàtic. El seu nom sol derivar del nom del jar, excepte si hi ha un META-INF/MANIFEST.MF al jar i conté un nom alternatiu amb la clau Automatic-Module-Name. En aquest cas existeix, i el contingut és “org.json”.
i compilar i executar així:
$ javac -p lib/stleary-json.jar -d classes `find src -name *.java`
$ java -p classes:lib/stleary-json.jar -m hello_modular/test.HelloModular
També podem crear un jar enlloc d’utilitzar la carpeta classes:
$ jar --create --file lib/hello-modular.jar -C classes .
$ java -p lib/hello-modular.jar:lib/stleary-json.jar -m hello_modular/test.HelloModular
Referències
- Java® Platform, SE & JDK Version 11 API specification
- Java. The Complete Reference (Ninth Edition). Herbert Schildt
- Capítol 20: Input/Output: Exploring java.io.
- Oracle Java SE Support Roadmap
- Composition vs. Inheritance: How to Choose?
- Difference between Association, Aggregation and Composition in UML, Java and Object Oriented Programming
- Practical Test Pyramid (Martin Fowler)
- Default methods
- Java SE 8 Date and Time
- Main Tools to Create and Build Applications
- Java Modules Cheat Sheet
Glossari Java
- Array
- Classe
- Classe abstracta
- Classe embolcall (wrapper class)
- Constructor
- Fil (thread)
- Interfície fluïda (fluent interface)
- Mètode
- Mètode d’instància
- Mètode de classe
- Multifil
- Objecte
- Objecte immutable
- Objecte mutable
- Procés
- Referència
- Secció crítica
- Sobrecàrrega (mètodes: Overloading)
- Sobreescriptura (mètodes: Overriding)
- Subclasse
- Superclasse
- Synchronized
- Tipus genèric
- Tipus primitiu
- Tipus cru (raw)
- Variable
- Variable d’instància
- Variable de classe
- Variable local
Array
Una col·lecció de dades del mateix tipus amb una posició designada per un sencer. Un array és un objecte del tipus Object.
Classe
Una plantilla que defineix la implementació d’un tipus d’objectes. Té membres: dades (variables d’instància) i funcionalitat (mètodes d’instància).
Classe abstracta
És un tipus de classe on certs detalls de la seva implementació es posposen per ser completats més tard. Poden ser classes que utilitzen el mot “abstract” o bé interfícies (mot “interface”).
Classe embolcall (wrapper class)
Són les classes que Java defineix per embolicar els tipus primitius. Són immutables.
Constructor
Un pseudo-mètode d’instància que crea un objecte. Tenen el nom de la classe, i s’invoquen amb “new”.
Fil (thread)
La unitat bàsica d’execució del programa. Un procés pot tenir diversos fils que funcionen simultàniament, cadascun realitzant un treball diferent, com ara esperar esdeveniments o realitzar un treball que requereix temps que el programa no necessita completar abans de continuar.
Interfície fluïda (fluent interface)
És una forma de dissenyar API orientades a objectes basada en l’encadenament de mètodes. L’objectiu és fer que la llegibilitat del codi font sigui més propera a la d’un text escrit.
Mètode
Una funció definida a una classe. Hi ha mètodes d’instància i de classe.
Mètode d’instància
Un mètode que s’invoca respecte a la instància d’una classe.
Mètode de classe
Un mètode que s’invoca sense referir-se a cap objecte particular. També es diu “mètode estàtic”.
Multifil
Descriu un programa que s’ha designat per tenir parts del codi que s’executen concurrentment.
Objecte
És una instància d’una classe, una concreció singular. També es diu “instància” segons el context.
Objecte immutable
Un objecte que NO pot ser modificat després de ser creat.
Objecte mutable
Un objecte que pot ser modificat després de ser creat. Habitualment tenen setters/getters.
Procés
Un espai d’adreces virtual que conté un o més fils.
Referència
Un tipus de variable on el seu valor és l’adreça d’un objecte.
Secció crítica
Un segment de codi en què un fil utilitza recursos (com certes variables d’instància) que altres fils poden utilitzar, però no ho poden fer alhora.
Sobrecàrrega (mètodes: Overloading)
Definició de dos mètodes amb el mateix nom i classe però diferents implementacions.
Sobreescriptura (mètodes: Overriding)
Quan una subclasse ofereix una implementació específica que ja s’ha oferit a una superclasse.
Subclasse
Classe que hereta. També classe fill.
Superclasse
Classe heretada. També classe pare.
Synchronized
Una paraula clau en el llenguatge de programació Java que, quan s’aplica a un mètode o bloc de codi, garanteix que, com a molt, un fil alhora executi aquest codi.
Tipus genèric
Un tipus genèric és una classe o interfície amb una secció de tipus de paràmetres envoltada per l’operador diamant <>.
Tipus primitiu
Un tipus de variable on el seu valor té una mida i format en concordança amb el seu tipus.
Tipus cru (raw)
Un tipus cru és una classe o interfície genèrica utilitzada sense paràmetres de tipus. No es recomana el seu ús.
Variable
Una dada amb un identificador. Té un tipus i un àmbit. Hi ha variables de classe, d’instància i locals. Poden ser de tipus primitiu o referència.
Variable d’instància
Una variable associada amb un objecte particular. També es diu “camp”.
Variable de classe
Una variable associada amb una classe, però amb cap instància particular. També es diu “camp estàtic”.
Variable local
Una variable coneguda dins d’un bloc, com per exemple dins d’un mètode.
Eines Java
junit
JUnit permet automatitzar les proves de codi en Java.
Una classe de proves en Java permet l’execució de proves unitàries sobre el codi que volem validar. A JUnit (5) podem indicar que una classe és de proves utilitzant anotacions davant dels mètodes d’aquesta classe. Principalment:
- @BeforeAll: mètode estàtic que s’executarà abans de totes les proves.
- @BeforeEach: mètode que s’executarà abans de cada prova.
- @Test: anotació més important, indica que el mètode és una prova a executar.
- @AfterEach: mètode que s’executarà després de cada prova.
- @AfterAll: mètode que s’executarà després de totes les proves.
Una prova (test) té un codi que pot fallar. Si no falla, la prova és correcta. Pot fallar per dues raons:
- Que hi hagi una excepció durant l’execució de la prova que no es gestioni al codi. Es comptabilitzen com a error.
- Que s’utilitzi una asseveració (assertion) de la llibreria JUnit, i que aquesta no es compleixi. Es comptabilitzen com a fallada (failure).
Les asseveracions de JUnit són crides a mètodes estàtics que poden resoldre’s amb aquest import (JUnit 5):
import static org.junit.jupiter.api.Assertions.*;
Aquestes són algunes de les asseveracions més importants:
- assertTrue(boolean): falla si el paràmetre és false
- assertFalse(boolean): falla si el paràmetre és true
- assertEquals(object1, object2): falla si els paràmetres no són iguals
- assertNotEquals(object1, object2): falla si els paràmetres són iguals
- fail(): falla sempre
Totes aquestes asseveracions permeten afegir un text explicatiu per ajudar-nos a entendre el problema que s’ha produït.
Els IDE moderns detecten quan una classe és de prova gràcies a que conté anotacions de @Test, i llavors permeten executar aquestes proves visualment, fent el recompte d’errors i permetent identificar en quina part del codi s’han produït.
A continuació es poden veure dues captures d’un error i una fallada a Eclipse.
Error
Hi ha una excepció a la prova testAddCredit(). La Failure Trace mostra quina excepció ha passat (primera línia) i la línia del codi on ha passat (ShopClientImpl.java).

Fallada
Hi ha asseveració que falla a la prova testJustCredit(). La Failure Trace mostra el missatge fallit de l’asseveració (primera línia), la línia del test on ha passat (ShopCartClientTest.java).

maven
Maven és una eina que permet gestionar la construcció i desplegament automàtics de projectes Java. Mitjançant un arxiu pom.xml, es descriu el procés de construcció i quines són les dependències d’altres projectes o llibreries. Aquestes dependències són automàticament descarregades d’un repositori central per ser utilitzades localment.
Maven té tres lifecycles predefinits:
- default (build): gestiona el desplegament
- clean: gestiona la neteja
- site: gestiona la documentació
Cada lifecycle conté una sèrie de fases consecutives. En particular, el default conté principalment:
- validate: valida la informació del projecte
- compile: compila el codi font
- test: proves unitàries del codi font
- package: empaqueta el codi font per a ser distribuït, habitualment JAR
- verify: proves d’integració
- install: instal.lació del paquet al repositori local (dependències locals)
- deploy: copiar el paquet a un repositori remot
Els plugins de maven afegeixen tasques o goals que s’especifiquen amb el format plugin-prefix:goal.
L’executable de maven es diu mvn, i se li poden passar fases o goals, que s’executen en l’ordre dels paràmetres. Les fases executen totes les fases precedents del mateix lifecycle.
Una línia de comanda típica seria:
$ mvn clean install
que executa les fases clean (lifecycle clean) i després install (lifecycle default). La fase install executa cadascuna de les fases anteriors del mateix lifecycle: validate, compile, test, package, verify i install. Aquestes fases tenen els seus goals associats en funció del packaging escollit.
Cada fase està feta d’una sèrie d’objectius o goals, que són tasques a executar. Per exemple, la fase compile està associada al plugin compiler i el goal compile. Hi ha goals que no estan associats a cap fase. Per exemple, hi ha el plugin exec-maven-plugin, que permet executar una classe Java. El plugin es diu exec, i el goal seria java. Per dir-li quina classe executar, cal indicar-ho amb la propietat exec.mainClass:
$ mvn exec:java -Dexec.mainClass="com.example.Main"
Maven utilitza una estructura de directoris predefinida:
/my-app
/src/main
/java => codi
/resources => recursos
/src/test
/java => proves
/resources => recursos de les proves
/target => arxius generats
/pom.xml => configuració
Aquest és el format típic d’un pom.xml:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>my_org</groupId>
<artifactId>example_project</artifactId>
<version>1.0-SNAPSHOT</version>
<name>example project</name>
<url>https://example.com</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>21</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.13</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.13</version>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20240303</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.11.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>
Explicació d’algunes seccions:
- groupId + artifactId identifiquen el paquet.
- La versió és 1.0-SNAPSHOT, SNAPSHOT indica que el codi no és estable (darrera versió).
- El packaging és JAR. Podria ser també WAR o EAR, per altres tipus d’entorns.
- Les properties permeten indicar la codificació dels arxius font (UTF-8) i la versió de java (11).
- La secció dependencies permet definir quines llibreries necessitarem perquè maven les descarregui i les utilitzi en els lifecycles. Indica sempre el groupId + artifactId (clau) i la versió.
- La secció build permet fer referència a plugins addicionals. Poden configurar-se amb la corresponent subsecció configuration (parameter), o bé a la secció general properties (user property).
Els paquets poden buscar-se a https://mvnrepository.com/, on podem trobar la sintaxi per a afegir-los a les dependencies. Aquí també podem veure les dependències de cada paquet, si en té, que també seran descarregades.
jps i jstack
Els podeu trobar a la vostra instal.lació del JDK, a la carpeta BIN on hi ha el java i el javac. JPS permet fer una llista dels processos java que s’estan executant:
$ jps
1648 org.eclipse.equinox.launcher_1.5.800.v20200727-1323.jar
6240 Main
5608 Jps
8636 DeadlockTest
Amb JSTACK podeu veure el contingut de les piles d’execució dels fils. Imagineu que DeadlockTest té dos fils, un que es diu loop i l’altre wait (s’han suprimit els fils que no interessen):
$ jps 8636
…
"wait" #12 prio=5 os_prio=0 tid=0x000002585443e000 nid=0x9d8 waiting for monitor entry [0x000000f255aff000]
java.lang.Thread.State: BLOCKED (on object monitor)
at m09uf2.test.DeadlockTest.lambda$3(DeadlockTest.java:85)
- waiting to lock <0x000000076b610938> (a m09uf2.test.DeadlockTest$MLong)
at m09uf2.test.DeadlockTest$$Lambda$2/245257410.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
"loop" #11 prio=5 os_prio=0 tid=0x000002585443d000 nid=0x29f8 waiting on condition [0x000000f2559fe000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at m09uf2.test.DeadlockTest.lambda$2(DeadlockTest.java:71)
- locked <0x000000076b610938> (a m09uf2.test.DeadlockTest$MLong)
at m09uf2.test.DeadlockTest$$Lambda$1/1044036744.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
…
Es pot veure l’estat i la línia on s’estan executant: BLOCKED (:85) i TIMED_WAITING (:71).
visualvm
El podeu instal.lar des de: https://visualvm.github.io/
Si l’executeu, i fent doble clic sobre el procés que voleu mirar, veureu:

Podeu veure diferents pestanyes:
- Monitor: mostra CPU, espai de memòria utilitzat (heap/metaspace), classes i fils.
- Threads: visualització gràfica dels fils que s’executen i l’estat en colors.
- Sampler: rendiment CPU i memòria de l’aplicació.
Programació Web
Conceptes d’HTML
Entendre com funciona l’HTML amb JavaScript és fonamental quan es construeixen aplicacions web. En aquesta guia, cobrirem l’estructura d’HTML, una llista més profunda d’etiquetes, com es carrega l’HTML en un navegador, la importància de l’etiqueta <script>, els atributs i propietats, i més.
Estructura i càrrega
L’HTML (HyperText Markup Language) estructura el contingut d’una pàgina web. Els navegadors analitzen l’HTML per mostrar el contingut, des de text i imatges fins a formularis i botons. Aquí tens com funciona l’estructura bàsica d’HTML:
<!DOCTYPE html>
<html lang="ca">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>La Meva Pàgina Web</title>
</head>
<body>
<h1>Benvingut!</h1>
<p>Això és un paràgraf de text.</p>
</body>
</html>
<!DOCTYPE html>: Indica al navegador que aquest document és HTML5.<html>: L’element arrel de tota la pàgina web.<head>: Conté metadades, inclòs el títol de la pàgina, el conjunt de caràcters, i recursos externs com fulls d’estil o scripts.<body>: Conté el contingut real que veuen els usuaris, com ara text, imatges i botons.
Quan obres un document HTML, el navegador el processa seguint aquests passos:
- Analitzar l’HTML: El navegador llegeix l’HTML, començant des de dalt. Construeix un arbre DOM (Document Object Model), una representació estructurada del document.
- Carregar recursos externs: A mesura que llegeix l’HTML, el navegador pot trobar enllaços a altres recursos com fulls d’estil CSS o scripts JavaScript i comença a carregar-los en paral·lel.
- Renderitzar el contingut: Quan l’arbre DOM està completament construït i els recursos externs han estat carregats, el navegador mostra la pàgina web.
Etiqueta script
La posició de l’etiqueta <script> dins del fitxer HTML és important perquè influeix en la rapidesa amb què apareix el contingut i en com interactuen els scripts amb el DOM.
Millors pràctiques:
- Col·locar els scripts abans del tancament del cos: Molt sovint, és millor col·locar les etiquetes
<script>just abans de l’etiqueta</body>perquè el contingut HTML es carregui primer i, després, el JavaScript.
<body>
<h1>La Meva Pàgina Web</h1>
<script src="script.js"></script>
</body>
-
Utilitzar els atributs defer o async: Aquests atributs ajuden a millorar el rendiment de la càrrega.
- defer: Garanteix que el script s’executa després que l’HTML estigui completament analitzat. És útil per a scripts que depenen que els elements del DOM estiguin preparats. No obliga situar el script després del body html, que fins i tot pot estar al head.
<script src="script.js" defer></script>- async: Carrega el script de manera asíncrona i l’executa tan aviat com es descarrega, sense esperar que l’HTML acabi de carregar-se. Utilitza aquest atribut per a scripts que no depenen del DOM.
<script src="script.js" async></script>
Elements HTML
Els elements HTML representen diferents tipus de contingut. Tenen una especificació HTML i una altra relacionada amb el DOM. Per exemple, l’especificació de l’etiqueta <input> està relacionada amb la de HTMLInputElement.
La majoria d’etiquetes permeten tenir contingut en el seu cos, i llavors tenen una etiqueta d’inici i una altra de final. Com per exemple, <div>content</div>. També tenim etiquetes de tipus void, que no permeten contingut intern. Per exemple, <img src="https://someurl">. Aquestes no tenen etiqueta de final, tot i que es tolera l’ús de l’etiqueta de self-closing: <img />. Altres etiquetes void: <br>, <hr>, <input>, <link>, <meta>.
Cada etiqueta té atributs i propietats específiques que modifiquen el comportament de l’element.
Els Atributs: formen part de l’etiquetatge HTML i defineixen valors o configuracions inicials quan es carrega la pàgina. Són estàtics i no canvien tret que es modifiquin explícitament a l’HTML o JavaScript.
Aquests són alguns atributs molt comuns:
- id: assigna un identificador únic a un element. S’utilitza per a estils (CSS) i scripts (JavaScript) per a dirigir-se a elements específics.
- class: assigna un o més noms de classe a un element. Les classes es poden compartir entre diversos elements per a l’estil de grup o la creació de seqüències de comandaments.
- style: especifica estils CSS en línia per a un element. Tot i que normalment es recomana utilitzar fulls d’estil externs o interns, els estils en línia poden ser útils per a l’estil dinàmic.
- data-*: s’utilitza per emmagatzemar atributs de dades personalitzats en un element. Això s’utilitza sovint juntament amb JavaScript per emmagatzemar o manipular dades al DOM.
Les Propietats: pertanyen al DOM (Document Object Model) i reflecteixen l’estat actual de l’element. Es poden actualitzar dinàmicament o accedir-hi mitjançant JavaScript.
Aquestes són algunes propietats molt comunes:
- id: reflecteix l’atribut del mateix nom.
- innerHTML: representa el contingut HTML dins d’un element. Podeu utilitzar aquesta propietat per obtenir o establir HTML dinàmicament dins d’un element. Aplicable a
<p>,<div>,<span>, etc. - textContent: represents the text content inside an element, without any HTML tags. It’s often used for retrieving or setting plain text. Aplicable a
<p>,<div>,<span>, etc. - innerText: representa el contingut de text dins d’un element, de manera similar a textContent, però amb una diferència clau:
innerTextrespecta l’estil CSS (comdisplay: none), de manera que només retorna text visible. També permet establir text sense format dins d’un element. Aplicable a<p>,<div>,<span>, etc. - style: es pot accedir a aquesta propietat o modificar-la mitjançant JavaScript per canviar dinàmicament les propietats CSS de l’element.
- classList: proporciona mètodes per afegir, eliminar o canviar classes en un element. És útil per manipular classes dinàmicament.
- value: reflecteix el valor dels elements relacionats amb el formulari com
<input>,<textarea>,<select>i<button>. Aquesta propietat s’utilitza per obtenir o establir el valor actual als camps d’entrada de l’usuari. - disabled: propietat booleana que serveix per desactivar la interacció dels elements relacionats amb el formulari.
JavaScript modern
- Javascript bàsic
- Funcions
- Execution contexts i event loop
- Orientació a objecte
- Javascript asíncron
- Modern things
- Referències
Javascript bàsic
Javascript és un llenguatge d’alt nivell que utilitza compilació just-in-time, o sigui, es compila durant la seva execució.
Javascript va començar com un llenguatge que permetia petites interaccions a les pàgines web, però esdevé important a partir de 2015 (versió ES6), quan comencen a fer-se revisions anuals de l’estàndard. ECMAScript (o ECMA-262) és el standard que fa interoperable Javascript als navegadors i al servidor (nodejs).
A la versió 2009 (ES5) es va introduir strict mode. Fins llavors, diem que utilitzàvem Javascript tradicional. El desenvolupament modern es fa exclusivament utilitzant aquest mode, ja que permet evitar molts bugs en ser més estricte: elimina errors silenciosos i prohibeix algunes sintaxis. Està activat per defecte en alguns casos (mòduls i objectes), però pot activar-se amb ‘use strict’ com a primera línia de codi d’un arxiu.
Variables
Javascript és un llenguatge de tipatge dinàmic: el tipus està associat al valor, no a la variable. Es pot canviar el valor d’una variable, i per tant el seu tipus.
A JS tradicional s’utilitza var per declarar variables. No cal inicialitzar, i llavors la variable té el valor undefined. La variable té l’àmbit de la funció on es declara.
A JS modern s’utilitzen let i const per a declarar variables i constants. La variable té l’àmbit del bloc on es declara.
Primitius
Els tipus de Javascript són els primitius i els objectes.
Els primitius essencials són:
boolean:trueofalse.null: una variable assignada intencionalment a res.undefined: una variable mai assignada.- numèrics: números de doble precisió.
NaNés un valor numèric no representable. - strings.
Els tipus primitius són immutables: no es poden modificar un cop creats. En canvi, els objectes (inclosos els arrays) són mutables. També se’ls diu tipus referència. Quan es comparen, són iguals si fan referència al mateix objecte. I quan s’assignen a una variable estem fent una nova referència al mateix objecte. Les còpies d’objectes poden ser superficials (referència) o profundes.
Conversions i operadors
Conversions:
- Podem fer conversions amb
Boolean(valor),Number(valor)iString(valor) - A número:
undefinedésNaN,falseinullés0,trueés1 stringa número: si buit,0, si no, s’intenta llegir (NaNen cas d’error)- A
boolean: sonfalseels valors0,stringbuit, els nullish (nulliundefined) iNaN(falsy values) i la restatrue(truthy values)
Operadors:
- Els operador matemàtics són
+ - * / % i ** - També hi ha l’operador concatenació per a strings
+ - L’operador
+unari equival aNumber(valor) - L’assignació
=és també un operador, i retorna el valor de l’expressió - Operadors modify-in-place per a tots els operadors aritmètics
+= -= ... - Increment/decrement
++ -- - Operadors de bits
& | ^ ~ << >> >>> - Operador coma retorna l’últim valor avaluat
- Operador condicional
? condition? value1 : value2retorna value1 si la condició éstrue ||troba el primer valor truthy&&troba el primer valor falsy??retorna el primer valor definit (nulllish coaslescing)?.retornaundefinedsi la propietat o mètode a continuació és nullish (optional chaining)??=només assigna a si l’operand a l’esquerra és nullish (nullish coalescing assignment)
Comparacions:
- Els operadors habituals
> < >= <= != ==(equals)===(strict equals) - “Strict equals” no fa conversió per a comparar
Objectes
Els objectes són col.leccions de propietats accessibles mitjançant claus. Les propietats poden tenir qualsevol tipus. És una organització de tipus mapa.
Els objectes es poden crear amb la notació literal { clau1: valor1, clau2: valor2...}.
Alguns objectes existents al llenguatge:
- Dates: representació de dates.
- Arrays: propietats accessibles amb un índex.
- Basats en claus: Maps, Sets.
La paradoxa dels primitius string, number i boolean és que també permeten mètodes utilitzant embolcalls anomenats String, Number i Boolean. Només es creen quan es criden els mètodes.
Els strings tenen aquests mètodes essencials: trim(), includes(), indexOf(), toUpperCase(), toLowerCase(), replace(), slice(), split(), repeat(), match(), charAt(), charCodeAt().
Quant als números: parseInt(), toString(), toExponential(), toFixed(), toPrecision(), valueOf(), toLocaleString(), parseFloat(), isInteger().
Els arrays són un tipus d’objecte que poden canviar de mida i contenir diferents tipus de dades. Estan indexats per sencers i permeten diverses operacions:
push(e): afegeix un element al final de l’array.shift()ipop(): extreu i retorna el primer / l’últim element de l’array,undefinedsi està buit.concat(arr1, ...): crea un nou array amb els continguts de dos o més arrays.slice(start, end): crea una còpia superficial d’una part d’un array (end, primer índex a excloure, és opcional).splice(start, deleteCount, item1, ...): canvia el contingut d’un array esborrant o substituint.
Operacions iteratives:
map(fn(e){...}): crea un nou array amb els valors retornats per la funció.find((testFn(e){...}): retorna el primer element que satisfà la funció de prova.findIndex(testFn(e){...}): retorna l’índex del primer element que satisfà la funció de prova.filter(testFn(e){...}): crea una còpia superficial del array amb els elements que satisfan la funció de prova.reduce(reducerFn(prev,e){}, initial): retorna un valor únic iterant pels elements i amb un valor previ. El primerpreviésinitial.
Comprovacions de tipus
L’operador typeof s’utilitza per determinar el tipus primitiu d’una variable. Retorna una cadena amb el nom del tipus, com ara string, number, boolean, object, undefined, function, or symbol.
console.log(typeof "hello"); // "string"
console.log(typeof 42); // "number"
console.log(typeof true); // "boolean"
console.log(typeof {}); // "object"
console.log(typeof undefined); // "undefined"
console.log(typeof function(){}); // "function"
L’operador instanceof comprova si un objecte és una instància d’una classe o funció constructora específica, retornant vertader o fals. És especialment útil per comprovar classes personalitzades, instàncies d’arrays i objectes de llibreria.
class Dog {}
const myDog = new Dog();
console.log(myDog instanceof Dog); // true
console.log([] instanceof Array); // true
console.log([] instanceof Object); // true (arrays are objects in JavaScript)
Iteració
Podem iterar sobre els arrays i els objectes.
array.forEach(fn(item, index))per a iterar sobre arrays.for...ofobté els valors per a iterables, com els arrays, strings, Map, Set i NodeList.for...inper a propietats enumerables d’objectes.Object.keys(obj)iObject.values()retornen arrays amb propietats i valors, respectivament.
Hoisting
El hoisting és un procés de l’intèrpret JS on sembla moure la declaració de variables, funcions i classes a dalt del seu àmbit abans d’executar el codi. Això permet, per exemple, utilitzar funcions abans de declarar-les. També es pot fer amb declaracions de variables i classes, però en general no es recomana.
Funcions
Les funcions són objectes que poden ser cridats. La declaració d’una funció té aquest aspecte:
function name(parameter1, parameter2, parameter3 = "some default value", ...) {
// body
}
Els paràmetres es comporten com variables arguments. Quan cridem la funció li passem arguments. Els arguments no definits en la crida tenen el valor undefined. Els paràmetres per defecte s’avaluen només si no s’indica l’argument corresponent.
El valor de retorn d’una funció que no retorna res és undefined.
Les variables locals són visibles només dins la funció. Les variables externes (o globals) també són accessibles. Si es diuen igual que alguna local, llavors perden l’accessibilitat.
Expressions de funcions
Les funcions admeten expressions. Per exemple, dirHola és una expressió de funció:
let dirHola = function(nom) {
console.log(`hola, ${nom}`);
};
dirHola('Joan');
function fesCrida(laMevaFuncio, nom) {
laMevaFuncio(nom);
}
fesCrida(dirHola, 'Anna');
La crida dins de fesCrida es fa exactament a la mateixa funció dirHola. El paràmetre laMevaFuncio té un valor de tipus funció i s’anomena callback, ja que permet cridar de tornada un codi.
Les declaracions de funcions es poden cridar abans de ser creades (hoisting), i només són visibles al block a que pertanyen. Les expressions de funcions només es poden cridar un cop assignades.
Funcions fletxa
Les funcions fletxa són una alternativa a les declaracions de funció que hem vist.
let func1 = (arg1, arg2, ...) => expression;
let func2 = (arg1, arg2, ...) => sentence;
let func3 = (arg1, arg2, ...) => {
// code with optional return sentence
}
Aquí func1 és una funció que retorna una expressió. Per exemple:
let suma = (a, b) => a + b;
suma(1,2);
En canvi func2 podria no retornar res, per exemple:
let logme = (a) => console.log(a);
logme(somevar);
Finalment func3 respon a la necessitat d’una funció multilínia.
Rest & Spread
Quan volem passar un nombre variable d’arguments podem utilitzar la sintaxi “resta de paràmetres”, que sempre va al final dels paràmetres d’una funció:
function someFunc(arg1, arg2, ...args) {
// ... aquí args és un array: args[0], args[1]...
}
L’acció inversa ens permetria passar un array com a paràmetres individuals. Per exemple, si volem utilitzar Math.max(arg1, arg2, ... argN) podem fer-ho així:
let arr = [1, 4, 9];
console.log(Math.max(...arr));
La sintaxi “spread” permet fer còpies d’arrays i d’objectes. Per exemple:
let arr = [1, 4, 9];
let arrCopy = [...arr];
let obj = { a: 1, b: 4, c: 9 }
let objCopy = {...obj};
let ObjCopyChanged = {...obj, a: 3} // a canvia a 3 (després de copiar 1)
Execution contexts i event loop

Les crides al Web API fan que hi hagi callbacks que s’hagin de cridar en certs moments (esdeveniments). Quan és així, la Web API afegeix aquestes crides a una cua de callbacks. L’event loop comprovarà quan està buit el stack per anar traient les callbacks i executant-les. En resum, JavaScript s’executa en un sol fil, i les callbacks ho fan quan el stack està buit.
Orientació a objecte
Un objecte es pot crear amb un inicialitzador:
const name = 'John';
const person = {
name: name,
getName: function() {
return this.name;
}
};
Una classe és una plantilla per a crear un objecte. Utilitzen internament funcions constructors i prototips.
class Person {
constructor (name) {
this.name = name;
}
getName() {
return this.name;
}
}
const person = new Person('Ann');
console.log(person.getName());
Aquesta seria l’alternativa amb funció constructor i prototips:
function Person(name) {
this.name = name;
}
Person.prototype.getName = function() {
return this.name;
};
Podem estendre una classe amb:
class JohnPerson extends Person {
constructor() {
super('John');
}
}
this
Només ens referirem al mode estricte.
- En el context global, this és undefined.
- En el context d’una funció, depèn de com es va cridar la funció:
- Cap valor, quan s’executa una funció aïllada.
- Al constructor d’una classe, la nova instància.
- A un mètode
objecte.metode(), l’objecte que el crida. - A una funció fletxa, el this del context pare.
Per al primer cas (1), hi ha una forma d’associar una funció a una instància: utilitzant el mètode bind(instància) de la funció es retornarà una nova funció que pot cridar-se aïlladament i on this és la instància.
Aquí, la crida a boundMyFunc assignarà this = myObj dins la meva funció:
function myFunc() {
// ...
}
let boundMyFunc = myFunc.bind(myObj);
boundMyFunc();
JSON
JSON és una representació en cadena (string) d’una estructura de dades que permet intercanviar-les fàcilment. La representació arrel pot ser un objecte {} o bé un array [], que poden contenir altres objectes, altres arrays o bé els tipus essencials: string, number, true/false o null.
JavaScript té dos mètodes que permeten convertir un objecte o array a un string JSON i també l’operació inversa:
- JSON.stringify(valor)
- JSON.parse(string)
A diferència dels literals JavaScript, la representació JSON sempre utilitza dobles cometes per als strings. A més, totes les propietats també han de tenir dobles cometes.
JSON.stringify no converteix qualsevol valor. Per exemple, no ho fa amb funcions o altres objectes, com Map. De vegades, cal implementar el segon i tercer paràmetres opcionals, replacer and reviver, per soportar certs tipus.
Javascript asíncron
Callbacks
L’API de JavaScript al navegador permet programar accions asíncrones. Per exemple, coses a fer quan hi ha un esdeveniment, o temporitzadors que executen altres accions. Per realitzar-les s’utilitzen callbacks, o sigui, valors de tipus funció.
Per exemple, setTimeout(callback, milliseconds) permet cridar una funció després d’un cert temps. Aquesta callback pot ser, per exemple, una funció fletxa:
setTimeout(() => {
console.log("ha passat 1 segon");
}, "1000")
Si volem executar una callback dins d’una altra es pot produir la “piràmide de la perdició”: una sèrie de funcions a dins d’altres funcions imbricades que fan el codi poc llegible. Per això van aparèixer les promeses.
Promises
El codi asíncron té habitualment dues parts:
- Un codi de producció que triga un cert temps a executar-se.
- Un codi de consumició que necessitarà allò produït un cop estigui llest.
Productor
Una promesa ajunta aquestes dues parts.
let promesa = new Promise(function(resolve, reject) {
// codi de producció
});
El codi de producció ha de fer una d’aquestes dues crides un cop acabi:
resolve(value)per indicar que tot va anar bé i el resultat ésvalue.reject(error)per indicar que no va anar bé i l’error éserror.
Per exemple, una promesa que acaba quan passa 1 segon amb el resultat “fet”:
let promise = new Promise(function(resolve, reject) {
setTimeout(() => resolve("fet"), 1000);
// error podria ser: reject(new Error("hi ha un error"))
});
Consumidor
Podem utilitzar el mètode then d’una promesa per a consumir-la:
promise.then(
function(result) { /* consumeix el resultat correcte */ },
function(error) { /* consumeix l'error */ }
);
// per exemple, per cridar alert amb el resultat (callback error no utilitzada):
promise.then(alert);
// es poden encadenar then, catch i finally:
promise.then(consumidorResultat).catch(consumidorError).finally(cridaSempre);
Si el consumidor del resultat retorna una promesa, es poden encadenar thens:
promise.then(value => {
return value.anotherPromise();
}).then(anotherValue => {
// use anotherValue
});
Si el que es retorna no és una promesa, l’API la converteix automàticament a promesa que resol al valor retornat:
const promise1 = new Promise((resolve, reject) => resolve(1));
promise1.then(v1 => v1 + 1).then(v2 => v2 *2).then(v3 => console.log(v3));
Es poden afegir diversos consumidors a un productor fent thens a la mateixa promesa:
promise.then(consumidorResultat1);
promise.then(consumidorResultat2);
Async/Await
Les promeses es poden utilitzar amb una sintaxi alternativa més amigable.
Per exemple, per dir que una funció retorna una promesa només cal prefixar-la amb async:
async function myAsyncFunc() {
return 1; // el mateix que Promise.resolve(1)
}
myAsyncFunc().then(alert); // mostra 1
Compte, perquè myAsyncFunc s’executarà de forma asíncrona ja que retorna una promesa. Per tant, si fem:
async function myAsyncFunc() {
return new Promise((resolve, reject) => {
setTimeout(() => resolve("done!"), 3000)
});
}
myAsyncFunc(); // retorna la promesa, que no utilitzem
console.log('crida feta!');
primer es mostrarà “crida feta!” i als tres segons es resoldrà la promesa, que ningú espera (no hi ha then).
Per altra banda, await espera fins que una promesa es resol i retorna el seu resultat. Només es pot utilitzar dins d’una funció prefixada amb async (també a mòduls):
async function myAsyncFunc() {
const promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("done!"), 3000)
});
const value = await promise;
return "well " + value;
}
myAsyncFunc().then(value => alert(value));
Modern things
Modules
Els mòduls a JavaScript són una forma de trossejar el codi per a trossejar-lo i fer-lo més fàcil de gestionar. Inicialment, el patró IIFE permetia crear mòduls. Altres frameworks han implementat aquest patró, com per exemple CommonJS en el cas de NodeJS. A partir d’ES6 tenim una implementació a la especificació.
Per crear un mòdul cal exportar des del mòdul A i importar-lo des del mòdul B. Hi ha dos tipus d’exports:
- default:
export default ...;. S’importen ambimport someNameOfYourChoice from './path/to/file.js';. - named:
export const someData = ...;. S’importen ambimport { someData } from './path/to/file.js';.
Un arxiu només pot contenir un mòdul default i un nombre il.limitat d’anomenats.
També es poden importar els anomenats amb import * as upToYou from './path/to/file.js'; i llavors pots accedir als elements exportats com upToYou.someData.
Template literals
També anomenada “string interpolation”, una cadena amb backticks permet incloure el resultat d’expressions:
let cadena = `bon dia, ${name}!`;
Shorthand syntax
La declaració shorthand permet assignar propietats d’un objecte on la clau i el valor tenen el mateix identificador, evitant haver de repetir ident: ident.
const name = 'Pere'
const age = 20
const location = 'Sabadell'
const user = {
name,
age,
location
}
Computed property names
Podem avaluar els noms de les propietats d’un objecte de forma dinàmica. Per exemple:
const obj = {
[someExpression]: value
}
Array & Object destructuring
El “destructuring” permet desempaquetar arrays i objectes en variables.
let arr = ["John", "Smith"];
let [firstName, surname] = arr;
A la dreta pot haver qualsevol dada iterable. A l’esquerra pot haver qualsevol assignable, per exemple:
let user = {};
[user.name, user.surname] = "John Smith".split(' ');
Si l’array és més llarg que la llista a l’esquerra, els ítems extra s’ignoren. Si l’array és més curt, els ítems de l’esquerra seran undefined. També podem ignorar ítems de la dreta escrivint comes seguides. I també podem utilitzar spread per a recollir la resta:
let [name1, name2, ...rest] = ["Rosa", "Joan", "Anna", "Pere"];
[user.name, user.surname] = "John Smith".split(' ');
Amb els objectes també ho podem fer:
let obj = {var1: "valor1", var2: "valor2"};
let {var1, var2} = obj;
// no hi ha ordre a les propietats!
Podem reanomenar una propietat i utilitzar valors per defecte. Seguint l’exemple anterior:
let obj = {var1: "valor1", var2: "valor2"};
let {var1: valor1, var2: valor2, var3 = "valor3"} = obj;
// "valor3" podria ser qualsevol expressió
Podem només extreure la part que ens interessa:
let obj = {var1: "valor1", var2: "valor2"};
let {var1} = obj;
Podem utilitzar la sintaxi “resta”:
let obj = {var1: "valor1", var2: "valor2", var3: "valor3"};
let {var1, ...rest} = obj;
// rest serà un objecte amb dos propietats, var2 i var3
Podem fer destructuring anidat:
let obj1 = {
var1: {
opt1: 1,
opt2: 2
},
var2: "valor2",
var3: ["first", "second", "third"]
};
let {
var1: { opt1, opt2 },
var3: [first, ...rest] } = obj1;
Podem utilitzar-ho per a passar paràmetres de funcions:
let obj = {var1: "valor1", var2: "valor2", var3: "valor3"};
function myFunc1({var1, var2}) {
// ...
}
myFunc1(obj);
let arr = ["first", "second"];
function myFunc2([var1, var2]) {
// ...
}
myFunc2(arr);
Referències
- javascript.info
- Client-Side Web Development
- Eloquent JavaScript
- Patterns
- ECMA-262
- Introducing JavaScript objects
- this, MDN
- What the heck is the event loop anyway? Philip Roberts
- Loupe
- Javascript Visualizer
Manipulació del DOM
El DOM (Document Object Model) és una representació d’un document HTML com a una estructura d’arbre, on cada node és un objecte que representa una part del document. El DOM es crea quan es carrega una pàgina web, i és manipulable des de JavaScript gràcies a una sèrie d’operacions que permeten:
- Afegir, modificar i esborrar qualsevol element o atribut HTML.
- Canviar qualsevol estil CSS.
- Reaccionar a un esdeveniment.
- Crear nous esdeveniments.
Navegació
Imaginem que tenim aquest senzill document:
<!doctype html>
<html lang="en-US">
<head>
<meta charset="utf-8" />
<title>Simple DOM example</title>
</head>
<body>
<section>
<img
src="dinosaur.png"
alt="A red Tyrannosaurus Rex" />
<p>
A link to the
<a href="https://www.mozilla.org/">Mozilla homepage</a>
</p>
</section>
</body>
</html>
Per poder manipular el DOM primer cal obtenir una referència a l’element i desar-la a una variable. Podem utilitzar Document.querySelector(), que demana un selector CSS com a paràmetre:
Per exemple:
const link = document.querySelector("a");
També tenim Document.querySelectorAll() per a obtenir una llista d’elements en format NodeList:
const paragraphs = document.querySelectorAll("p");
paragraphs.forEach((paragraph) => {
// do something with it
});
Tot i que amb aquests dos mètodes en tenim prou per fer queries, hi ha altres (menys flexibles) com Document.getElementById(), Document.getElementsByTagName(), Document.getElementsByClassName(), etc. També podem veure els elements fill amb la propietat children, que retorna una col.lecció HTMLCollection.
Si volem moure’ns cap a dalt, podem utilitzar la propietat parentElement o el mètode closest(selector). També ens podem moure cap als costats amb nextElementSibling i previousElementSibling.
Selectors CSS
Aquests són els selectors més habituals:
- Tots:
* - Etiqueta:
head - Classe:
.red - ID:
#nav - Etiqueta i classe:
div.row - Valor d’atribut:
[aria-hidden="true"] - Fills d’un altre element:
li a - Fills directes:
li > a - Tots dos selectors:
li, a
Pseudo-selectors:
- Primer fill:
:first-child - Últim fill:
:last-child - Element amb hover o focus:
:hover,:focus - Element clicat:
:active - Enllaços no clicats o clicats:
:link,:visited
Modificació
També podem afegir nous nodes al DOM. Si es volgués afegir un paràgraf a la secció seria així:
const sect = document.querySelector("section");
const para = document.createElement("p");
para.textContent = "We hope you enjoyed the ride.";
sect.appendChild(para);
També podem esborrar un element de diferents formes:
sect.removeChild(para);
para.remove();
para.parentNode.removeChild(para);
Una altra opció és utilitzar un template de l’html i clonar-lo al contingut.
<main class="container">
<h1>Template!</h1>
</main>
<template id="message">
<section>
<h2 class="heading"></h2>
<p class="text"></p>
</section>
</template>
const template = document.querySelector("#message");
const message = template.content.cloneNode(true);
message.querySelector('.heading').textContent = 'A title';
message.querySelector('.text').textContent = 'Some text here';
const main = document.querySelector('main');
main.appendChild(message);
Atributs i propietats
Els elements tenen atributs i propietats:
- Els atributs són les característiques dels elements que apareixen dins de l’etiqueta, i es defineixen a l’HTML. S’anomenen amb paraules separades per guions. Exemple:
src,alt. - Les propietats defineixen el comportament intern i la funcionalitat d’un element, i es manipulen des de JS. S’anomenen amb camel case. Exemple:
valueoinnerHTML.
Tenim Element.getAttribute(), Element.setAttribute(), Element.removeAttribute() i Element.hasAttribute() per a gestionar els atributs d’un element. Les propietats, en canvi, es manipulen com a propietats de l’objecte.
Podem tenir atributs que tenen una representació com a propietats. Per exemple, id és un atribut i propietat. Normalment es mantenen sincronitzats:
p.setAttribute("id", "one");
let id1 = p.getAttribute("id"); // one
let id2 = p.id; // one
En canvi, les propietats d’un formulari modificables per l’usuari (value, checked, selected) no estan sincronitzades: l’atribut és el valor inicial de l’HTML i la propietat, l’actual. De fet, Document.setAttribute() només canvia el valor si no ho ha fet l’usuari, però value ho fa sempre.
La manipulació d’estils es realitza accedint a la propietat HTMLElement.style. També és molt útil la propietat Element.classList, que retorna una DOMTokenList. Aquesta permet afegir i esborrar classes d’un element amb els mètodes add i remove, per exemple.
Atributs data
Val la pena parlar dels atributs data. Se solen utilitzar des de JS, en contraposició als atributs HTML, ja que no tenen significat a l’hora de visualitzar un element.
<div class="expand" data-expand>
<p>Some content that can be collapsed or expanded.</p>
<button data-click="sayHi">Say Hello!</button>
<button data-click="showMore">Show More</button>
</div>
let accordion = document.querySelector('[data-expand]');
let btnHi = document.querySelector('[data-click="sayHi"]');
let btnMore = document.querySelector('[data-click="showMore"]');
En general, si volem manipular amb JS els elements del DOM, és preferible utilitzar atributs data, que no condicionen la creació d’IDs o classes, que habitualment associem als estils CSS.
Esdeveniments
Podem afegir esdeveniments associats a un node del DOM utilitzant EventTarget.addEventListener(). La sintaxi més habitual inclou dos paràmetes:
- El
typeindica quin tipus d’esdeveniment vol escoltar-se. - El listener sol ser una funció callback que rep un objecte Event. Aquest objecte té tres propietats interessants:
type, el tipus d’esdeveniment;target, l’element que ha generat l’esdeveniment; icurrentTarget, el que té associat el listener. Aquestes dues últimes poden ser diferents perquè els esdeveniments són bombolles que pugen.
Exemple de l’efecte bombolla.
<main class="container">
<h1>Bubbling!</h1>
<button id="b1">First!</button>
<button id="b2">Second!</button>
</main>
const main = document.querySelector("main");
const btn1 = document.querySelector("#b1");
const btn2 = document.querySelector("#b2");
function handle(e) {
console.log(`${e.type} target: ${e.target.tagName}, current: ${e.currentTarget.tagName}`);
}
document.body.addEventListener("click", handle);
main.addEventListener("click", handle);
btn1.addEventListener("click", handle);
Píndoles JavaScript
JSDoc
JSDoc permet fer anotacions a JavaScript per a fer comprovacions de tipus i documentar el codi. Si s’utilitza VSCode podem activar aquesta comprovació afegint un arxiu jsconfig.json amb les propietats strict i checkJs:
{
"compilerOptions": {
"strict": true,
"checkJs": true
}
}
Variables
Podem definir el tipus d’una variable:
/** @type {number} */
let result1;
Si la definició té una assignació, JSDoc infereix el seu tipus i no caldria indicar-lo:
let result2 = 0;
Tot i que JavaScript ho permetria, JSDoc no permet canviar el tipus d’una variable.
Funcions
Podem documentar la funció, els seus paràmetres i el valor retornat.
/**
* Add two numbers
* @param {number} number1
* @param {number} number2
* @returns {number}
*/
function add(number1, number2) {
return number1 + number2;
}
JSDoc mostrarà un problema si intentem utilitzar aquesta funció amb un paràmetre que no sigui de tipus number, o bé si intentem assignar el resultat a una variable que no sigui number.
Tipus
Podem indicar els tipus entre dues claus.
- Els primitius:
boolean,string,number. - Els arrays, amb les claus quadrades, p. ex:
string[]. - Els objectes es poden definir amb
@typedef.
/**
* User object, surname is optional.
* @typedef {Object} User
* @property {number} id
* @property {string} name
* @property {string} [surname]
* @property {boolean} active
*/
- Les funcions callback es poden definir amb
@callback, l’equivalent atypedefper a callbacks:
/**
* A callback function applicable to a number
* @callback ApplyFunc
* @param {number} number
* @returns {number}
*/
O bé directament utilitzant el tipus function:
/** @type {function(number):string} */
let stringerFn = (num) => (num.toString());
- Podem definir que un paràmetre o variable pot tenir més d’un tipus amb l’OR.
/** @type {number | undefined} */
- Podem fer casting de tipus:
const inputEl = /** @type {HTMLInputElement} */ (document.querySelector('input'));
És important utilitzar parèntesis sobre l’element castejat
- Podem fer tipus condicional:
/**
* @returns {string | number}
*/
function getValue() {
return Math.random() > 0.5? String(17) : 17;
}
const value = getValue();
if (typeof value === 'string') {
// type is string for this block
console.log(`length is ${value.length}`);
}
if (typeof value === 'number') {
// type is number for this block
console.log(`square is ${value*value}`);
}
Desactivació d’errors
Podem desactivar l’error de la línea següent amb:
// @ts-ignore
Podem desactivar tots els errors d’un arxiu amb:
// @ts-nocheck
o activar-los per a un arxiu, si per defecte estan desactivats, amb:
// @ts-check
Precedència d’operadors
| Precedence | Operator type | Associativity | Individual operators |
|---|---|---|---|
| 18 | Grouping | n/a | ( … ) |
| 17 | Member Access | left-to-right | … . … |
| Optional chaining | … ?. … |
||
| Computed Member Access | n/a | … [ … ] |
|
new (with argument list) |
new … ( … ) |
||
| Function Call | … ( … ) |
||
| 16 | new (without argument list) |
n/a | new … |
| 15 | Postfix Increment | n/a | … ++ |
| Postfix Decrement | … -- |
||
| 14 | Logical NOT (!) | n/a | ! … |
| Bitwise NOT (~) | ~ … |
||
| Unary plus (+) | + … |
||
| Unary negation (-) | - … |
||
| Prefix Increment | ++ … |
||
| Prefix Decrement | -- … |
||
typeof |
typeof … |
||
void |
void … |
||
delete |
delete … |
||
await |
await … |
||
| 13 | Exponentiation (**) | right-to-left | … ** … |
| 12 | Multiplication (*) | left-to-right | … * … |
| Division (/) | … / … |
||
| Remainder (%) | … % … |
||
| 11 | Addition (+) | left-to-right | … + … |
| Subtraction (-) | … - … |
||
| 10 | Bitwise Left Shift (<<) | left-to-right | … << … |
| Bitwise Right Shift (>>) | … >> … |
||
| Bitwise Unsigned Right Shift (>>>) | … >>> … |
||
| 9 | Less Than (<) | left-to-right | … < … |
| Less Than Or Equal (<=) | … <= … |
||
| Greater Than (>) | … > … |
||
| Greater Than Or Equal (>=) | … >= … |
||
in |
… in … |
||
instanceof |
… instanceof … |
||
| 8 | Equality (==) | left-to-right | … == … |
| Inequality (!=) | … != … |
||
| Strict Equality (===) | … === … |
||
| Strict Inequality (!==) | … !== … |
||
| 7 | Bitwise AND (&) | left-to-right | … & … |
| 6 | Bitwise XOR (^) | left-to-right | … ^ … |
| 5 | Bitwise OR (|) | left-to-right | … | … |
| 4 | Logical AND (&&) | left-to-right | … && … |
| 3 | Logical OR (||) | left-to-right | … || … |
| Nullish coalescing operator (??) | … ?? … |
||
| 2 | Assignment | right-to-left | … = … |
… += … |
|||
… -= … |
|||
… **= … |
|||
… *= … |
|||
… /= … |
|||
… %= … |
|||
… <<= … |
|||
… >>= … |
|||
… >>>= … |
|||
… &= … |
|||
… ^= … |
|||
… |= … |
|||
… &&= … |
|||
… ||= … |
|||
… ??= … |
|||
| Conditional (ternary) operator | right-to-left (Groups on expressions after ?) |
… ? … : … |
|
| Arrow (=>) | right-to-left | … => … |
|
yield |
n/a | yield … |
|
yield* |
yield* … |
||
| Spread (...) | ... … |
||
| 1 | Comma / Sequence | left-to-right | … , … |
TypeScript
- Introducció
- Des de JSDoc
- Transició des de JavaScript
- Build step
Introducció
TypeScript és un superset de JavaScript que afegeix tipat estàtic al llenguatge. Això significa que pots especificar tipus per a variables, paràmetres de funció i retorns de funció, cosa que ajuda a evitar molts errors comuns de programació.
Què afegeix TypeScript:
- Annotations de tipus: TypeScript et permet especificar tipus (per exemple, number, string, boolean, tipus personalitzats) per a variables, paràmetres i valors de retorn, cosa que fa que TypeScript pugui detectar errors en temps de compilació en lloc de temps d’execució.
let age: number = 30; // `age` can only be a number
- Interfaces i alies de tipus: Pots definir l’estructura dels objectes i especificar com han de ser les estructures de dades, fent que el codi sigui més estructurat i previsible.
interface User {
name: string;
age: number;
}
- Enums: TypeScript afegeix enums per definir constants amb nom, fent que el codi sigui més llegible i evitant valors no vàlids.
enum Direction {
North,
South,
East,
West
}
-
Classes i interfícies amb tipat estricte: TypeScript admet classes i interfícies fortament tipades, ajudant a assegurar un ús consistent de classes i objectes en tota l’aplicació.
-
Genèrics: TypeScript afegeix genèrics per escriure funcions i classes reutilitzables i tipus segurs.
function wrapInArray<T>(value: T): T[] {
return [value];
}
Aquests són els avantatges per al desenvolupador:
-
Detecció primerenca d’errors: TypeScript detecta errors durant el desenvolupament, cosa que ajuda a reduir errors i problemes en temps d’execució. Això és especialment útil en projectes més grans.
-
Millora en llegibilitat i mantenibilitat: Amb anotacions de tipus, els desenvolupadors poden entendre amb més facilitat la funció i els valors esperats de variables i funcions, fins i tot quan revisiten el codi després de molt temps.
-
Millor autocompleció i eines: Editors com VSCode poden proporcionar millor autocompleció, suport per a la refactorització i eines de depuració, tot impulsat pel sistema de tipus de TypeScript.
-
Escalabilitat: L’estructura i la seguretat de tipus de TypeScript el fan ideal per a projectes més grans i equips, on el codi ha de ser fàcilment llegible i mantenible per múltiples desenvolupadors.
-
Compatibilitat amb JavaScript: TypeScript es compila en JavaScript pur, així que es pot utilitzar en qualsevol entorn on s’executi JavaScript, incloent navegadors web i Node.js.
Aquests serien alguns inconvenients:
-
Corba d’aprenentatge: TypeScript introdueix sintaxi i conceptes addicionals, que poden requerir temps perquè els desenvolupadors de JavaScript els aprenguin i s’hi adaptin.
-
Pas Addicional de compilació: TypeScript necessita compilar-se a JavaScript, cosa que afegeix un pas de compilació que pot alentir el desenvolupament en alguns fluxos de treball.
-
Sintaxi més estricta: La rigorositat de TypeScript pot semblar pesada per a prototips ràpids o petits scripts on els beneficis del tipat rigorós poden no ser tan evidents.
-
Possibilitat de sobrecàrrega: En projectes petits o tasques de desenvolupament ràpid, l’estructura i definicions de tipus de TypeScript poden de vegades semblar una feina extra sense grans beneficis.
Des de JSDoc
TypeScript i JSDoc milloren ambdós JavaScript amb seguretat de tipus, però difereixen en l’enfocament i les capacitats. TypeScript ofereix un sistema de tipus integrat, que permet la detecció d’errors anticipada en temps de compilació, un excel·lent suport d’eines i escalabilitat per a projectes grans, tot i que requereix un pas de compilació i té una corba d’aprenentatge més pronunciada. En canvi, JSDoc permet anotacions de tipus directament a JavaScript sense compilació, fent-lo flexible i més senzill d’implementar per a projectes petits o migracions graduals, però proporciona una comprovació de tipus limitada i manca les característiques completes de TypeScript, cosa que el fa menys adequat per a codi més gran.
Quan tenim un codi amb JSDoc que volem canviar a TypeScript, començar per reanomenar tots els arxius de .js a .ts i treure totes les anotacions de JSDoc. Compte, els imports han d’utilitzar el nom de l’arxiu .js.
Transició des de JavaScript
Tipus de variables
Afegeix tipus utilitzant : type després del nom de variable:
let message: string = "Hello, world!";
let count: number = 10;
Tenim dos tipus especials: unknown i any.
El tipus any permet ignorar el sistema de comprovació de tipus, ja que pots fer qualsevol operació sense que es mostrin errors. S’hauria d’evitar sempre que es pugui. Exemple:
let value: any = "Hello";
value = 42; // No error
value.toUpperCase(); // No error, even if value is not a string at runtime
El tipus unknown és una alternativa més segura a any. Pot contenir qualsevol tipus, però obliga a fer comprovació de tipus o assercions abans d’utilitzar-ho.
let value: unknown = "Hello";
if (typeof value === "string") {
console.log(value.toUpperCase()); // Safe to call string methods here
}
TypeScript intentarà inferir els tipus segons l’ús que es faci de les variables i les funcions. El consell és treballar amb el compilador en mode estricte (veure la configuració tsconfig.json suggerida més endavant), perquè així es queixarà si hi ha algun tipus indefinit. Alguns casos d’inferència de tipus:
let name = "Alice"; // TypeScript infers `name` as `string`
function add(a: number, b: number) {
return a + b; // TypeScript infers the return type as `number`
}
const numbers = [1, 2, 3];
numbers.forEach(num => console.log(num * 2)); // `num` is inferred as `number`
Aquests són casos on el compilador no pot inferir el tipus:
function greet(name) {
return `Hello, ${name}!`; // 'name' has an implicit 'any' type, but 'strict' mode warns
}
try {
// Some code that might throw an error
} catch (error) {
console.log(error.message); // Error: Object is of type 'unknown'
}
const person = { name: "Alice", age: 25 };
const key = "name" as string;
console.log(person[key]); // Error: expression of type 'string' can't be used to index type
A l’últim cas, la solució té a veure amb utilitzar keyof:
type Person = { name: string; age: number };
const person: Person = { name: "Alice", age: 25 };
const key: keyof Person = "name"; // Allowed, because `key` is of type `"name" | "age"`
Funcions
Afegeix : type darrere dels parèntesis de la funció:
function greet(name: string): string {
return `Hello, ${name}`;
}
function logMessage(message: string): void {
console.log(message);
}
Arrays i objectes
Per a arrays, especifica el tipus seguit de []:
let numbers: number[] = [1, 2, 3];
let names: string[] = ["Alice", "Bob"];
Per a objectes, defineix l’estructura de cada propietat:
let person: { name: string; age: number } = {
name: "Alice",
age: 25
};
Tuples
Una tupla de TypeScript és un array de mida fixa on cada element pot tenir un tipus diferent:
const t1: [number, string] = [1, "Alice"];
const t2: [number, string] = [...t1]; // spreading a tuple
t2[0] = 2; t2[1] = 'Bob'; // mutating a tuple
const t3: unknown = [3, "Eve"]; // tuple without type
const t4 = t3 as [number, string]; // as a tuple
const t5 = t3 as (number | string)[]; // as an array
Interfícies i tipus
Una interfície defineix la forma d’un objecte reusable, de forma molt similar als tipus:
interface PersonI {
name: string;
age: number;
}
type PersonT = {
name: string;
age: number;
};
let person1: PersonI = { name: "Alice", age: 25 };
let person2: PersonT = { name: "Bob", age: 27 };
Les interfícies i els tipus es poden estendre:
// interfaces
interface Animal {
species: string;
}
interface Dog extends Animal {
breed: string;
}
// types
type Animal = {
species: string;
};
type Dog = Animal & {
breed: string;
};
Els tipus permeten definir primitius, unions i interseccions:
type ID = string | number; // Union type
type Coordinates = { x: number; y: number } & { z: number }; // Intersection
Les interfícies permeten la seva implementació per una classe:
interface Person {
name: string;
age: number;
greet(): void;
}
class User implements Person {
constructor(public name: string, public age: number) {}
greet() {
console.log(`Hello, my name is ${this.name}.`);
}
}
Tipus Union
Les tipus Union permeten una variable acceptar diversos tipus:
function display(value: string | number): void {
console.log(value);
}
Propietats i paràmetres opcionals
Les propietats i paràmetres opcionals s’indiquen amb ? després del nom:
type User = {
name: string;
age?: number; // age is optional
};
function greet(name: string, greeting?: string): string {
return `${greeting || "Hello"}, ${name}`;
}
El tipus de greeting serà string | undefined.
Tipus de funcions
Indica en el tipus d’un callback els tipus de l’entrada i la sortida:
function process(data: string, callback: (input: string) => void): void {
callback(data);
}
function processAndReturn(data: number, callback: (input: number) => number): number {
return callback(data);
}
Pots utilitzar tipus:
type Transformer = (input: string) => string;
function transformData(data: string, transformer: Transformer): string {
return transformer(data);
}
Per a utilitzar genèrics:
function mapArray<T, U>(array: T[], callback: (item: T) => U): U[] {
return array.map(callback);
}
const words = ["hello", "world", "typescript"];
const wordLengths = mapArray(words, (item) => item.length);
console.log(wordLengths); // Output: [5, 5, 9]
Comprovacions de tipus
Per assegurar-te que una variable tingui el tipus esperat, pots utilitzar diferents tipus de type guards en TypeScript. A continuació, s’expliquen les tècniques més comunes.
Comprovació per veritat (truthy)
Quan es comprova si una variable no és nul·la ni undefined, pots refinar el tipus simplement verificant si és “truthy”:
let str: string | null;
// ...
if (str) { // str no és null ni undefined aquí
console.log(str.length);
}
Tot i que pots utilitzar l’operador ! per indicar que una variable no és nul·la ni undefined, això pot ser arriscat i cal evitar-ho sempre que sigui possible:
let value: string | null = getValueFromSomewhere();
console.log(value!.length); // Es tracta com no nul·la, però podria fallar si no ho és.
Explícit amb as
Pots informar el compilador d’un tipus específic utilitzant el type assertion as. Això és útil, però cal fer-ho amb precaució, ja que el compilador confia completament en tu:
let value: any = "This is a string";
let length: number = (value as string).length; // Narrowing explícit
Amb typeof (només primitius)
L’operador typeof permet verificar el tipus d’una variable per assegurar que és un dels tipus primitius com string, number, boolean, etc.:
let str: unknown;
// ...
if (typeof str === 'string') { // str és un string aquí
console.log(str.length);
}
Amb instanceof (només classes)
L’operador instanceof s’utilitza per verificar si un objecte és una instància d’una classe concreta:
class Dog {
bark() { console.log("Woof!"); }
}
class Cat {
meow() { console.log("Meow!"); }
}
let pet: Dog | Cat;
// ...
if (pet instanceof Dog) {
pet.bark();
} else {
pet.meow();
}
Amb in
El in permet comprovar si una propietat existeix en un objecte. Això és útil quan treballes amb tipus d’objecte que tenen propietats exclusives:
interface Car {
drive: () => void;
}
interface Boat {
sail: () => void;
}
let vehicle: Car | Boat;
// ...
if ("drive" in vehicle) {
vehicle.drive();
} else {
vehicle.sail();
}
Amb tipus literals
Pots refinar literalment el tipus de les variables amb operadors d’igualtat (=== o !==) per a unions de literals:
type Shape = "circle" | "square";
function drawShape(shape: Shape) {
if (shape === "circle") {
console.log("Dibuixant un cercle");
} else {
console.log("Dibuixant un quadrat");
}
}
Amb unions discriminades
Quan treballes amb unions de tipus d’objecte que tenen una propietat comuna (el discriminant), pots utilitzar aquesta propietat per refinar el tipus:
interface Circle {
kind: "circle";
radius: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
type Shape = Circle | Rectangle;
function getArea(shape: Shape) {
if (shape.kind === "circle") {
return Math.PI * shape.radius ** 2;
} else {
return shape.width * shape.height;
}
}
Amb funcions personalitzades
Pots crear els teus propis type guards amb una funció que retorni un tipus de la forma variable is Type:
interface Fish {
swim: () => void;
}
interface Bird {
fly: () => void;
}
function isFish(animal: Fish | Bird): animal is Fish {
return (animal as Fish).swim !== undefined;
}
function move(animal: Fish | Bird) {
if (isFish(animal)) {
animal.swim();
} else {
animal.fly();
}
}
Control de flux automàtic
TypeScript també pot refinar tipus basant-se en el control de flux sense necessitat de guards explícits. Per exemple, en verificar valors null o undefined:
function processValue(value: string | null | undefined) {
if (!value) {
console.log("Valor no proporcionat");
} else {
console.log("Longitud del valor:", value.length);
}
}
Build step
Per a gestionar un projecte amb TypeScript des de zero, cal preparar un package.json mitjançant npm, instal.lar TypeScript per a desenvolupament i generar la seva configuració. Aquests serien els passos habituals:
$ npm init -y # creates an empty package.json
$ npm install typescript --save-dev # installs typescript package for development
$ npx tsc --init # inits the tsconfig.json file with compiler options
Alternativament, podem editar directament dos arxius de text amb els paràmetres necessaris:
package.json
{
"name": "your-project",
"version": "0.1.0",
"scripts": {
"compile": "tsc",
},
"devDependencies": {
"typescript": "^5.5.4"
}
}
tsconfig.json
{
"compilerOptions": {
"target": "es6",
"strict": true,
"esModuleInterop": true,
"outDir": "dist",
"sourceMap": true
},
"include": [
"src/**/*",
],
"exclude": [
"node_modules"
]
}
Això vol dir que els arxius TypeScript els trobarà a src. La carpeta node_modules cal ignorar-la, ja que inclou tots els packages instal.lats de les dependències.
Aquesta configuració també indica que utilitzarà el mode estricte (strict: true). És una pràctica desitjada, ja que implica escriure codi amb bones pràctiques. Principalment, obliga a fer anotacions de tipus per a les variables i comprovacions de null per als objectes. I no infereix mai que el tipus de la variable és any si no ha trobat una anotació.
Per a instal.lar les dependències (en aquest cas, typescript) caldrà fer:
$ npm install
Llavors podem compilar a JavaScript així:
$ npm run compile
I tots els arxius js es trobaran a dist.
Resumint, aquesta seria l’estructura de carpetes:
/your-project
├── /dist // compiled js files
├── /node_modules // packages installed by npm
├── /src // ts files
├── package.json // npm config file
├── tsconfig.json // compiler configuration
Persistència de dades
Conceptes de persistència
- Localització de les dades
- Model de dades
- Què, quan i com persistir
- Quin tipus de persistència?
- Base de dades d’aplicació o d’integració
- Model relacional vs NoSQL
- Característiques ACID
- Característiques BASE
Localització de les dades
Una aplicació conté codi i dades. El codi interactua amb les dades que hi ha a la memòria principal, que és volàtil, el tipus de memòria més ràpida que existeix i la més senzilla de llegir i escriure. A POO, les dades són als objectes.
Un problema de la memòria principal és que perdem el seu contingut si l’aplicació finalitza, ja sigui de forma correcta o per un problema. El context necessari per a l’execució d’una aplicació és el seu estat, i cal recuperar-lo cada cop que les executem. Això es fa utilitzant memòria no volàtil basada en un o diversos arxius físics.
Quan parlem de recuperar el context d’una aplicació per a la seva execució, no sempre és possible fer-ho amb totes les dades que es gestionen. Pot ser perquè la mida sigui superior a la de la memòria principal, però també perquè no calgui i sigui millor ser mandrós en la seva càrrega per raó de rendiment.
Model de dades
El disseny del model de dades ha de tenir en compte el millor rendiment possible quan s’utilitza memòria no volàtil, molt més lenta, i facilitar la futura escalabilitat de les nostres dades.
Un mecanisme per a minimitzar temps és la memòria cache: mantenim un nombre limitat d’objectes a la memòria principal per evitar haver-los de carregar des de la memòria no volàtil cada cop, i els invalidem si es modifiquen.
També els SGBD poden proporcionar-nos mecanismes per a processar les dades sense necessitat de carregar-les en memòria, gràcies a llenguatges declaratius com SQL: demanem quines dades necessitem, no com processar-les.
Si parlem de disseny, és convenient crear una capa de persistència que encapsuli el procés. Això ens permetrà substituir-la sense afectar la resta de l’aplicació. Si parlem d’usabilitat, la persistència hauria de ser transparent per a l’usuari.
Què, quan i com persistir
Tenim dos mètodes de persistència: d’estat, el més habitual, i basat en esdeveniments.
Persistència d’estat
Parlant de POO, ens podríem fer la pregunta de si allò que volem persistir és un objecte o una estructura de dades. Els objectes tenen comportament i encapsulen les dades. Les estructures de dades exposen les dades i no tenen comportament. Aquesta distinció ens ajudarà a decidir la nostra estratègia de persistència.
Per definir la persistència, cal repassar els camps de l’objecte i decidir quins necessitarem per tal de tornar a instanciar l’objecte en memòria principal. Alguns camps innecessaris poden ser calculats o tenir una funció temporal. Podem ajudar-nos de l’estudi dels constructors i setters de l’objecte.
A més, ens caldrà fer referències entre els objectes. Quan dissenyem mecanismes ad-hoc, podem utilitzar un identificador d’objecte en el moment de la persistència. En el cas dels SGBD, els objectes es relacionen amb claus foranes.
Tenim diversos àmbits de persistència:
- Context general de l’aplicació: configuracions generals, per usuari, etc.
- Documents, basats en serialització. Persistim en base a un document que pot intercanviar-se.
- Registres, habitualment relacionats i associats a SGBD. No se sol restaurar completament al començament, només quan cal.
Persistència d’esdeveniments
La persistència per esdeveniments (Event Sourcing) és un mecanisme que permet restaurar l’estat d’una aplicació a partir de tots els esdeveniments que s’han produït. Es tracta d’una mena de log on es guarden els esdeveniments per ordre d’ocurrència, i que permet reconstruir l’estat en qualsevol moment del temps.
Quan l’aplicació s’inicia, reconstrueix l’estat actual aplicant tots els esdeveniments en l’ordre en que es val produir. Com que podria haver molts d’esdeveniments, aquest mecanisme se sol combinar amb altres, com per exemple emmagatzemar un estat intermedi (snapshot) i aplicar els esdeveniments a partir d’aquell instant.
Quan i com
Hem de persistir sempre que hi hagi un canvi en alguna dada? La resposta general és que sí. Així evitem perdre dades si el nostre programa falla abans d’haver persistit. Però de vegades no seria òptim, parlant de rendiment, haver de persistir tot l’estat d’una aplicació. Alternativament, es podria endarrerir el moment, o fer-ho periòdicament de forma automàtica. També podem deixar aquesta responsabilitat en mans de l’usuari (File > Save).
La persistència per esdeveniments permet només guardar l’esdeveniment que es produeix en lloc de totes les dades, i també pot ser una solució per millorar el rendiment.
Quin tipus de persistència?
El format i organització dependrà de les nostres necessitats.
Criteris per decidir la forma de persistència:
- És una aplicació monousuari o multiusuari?
- Es comparteix informació a la xarxa amb altres clients o aplicacions?
- Hi ha un volum molt alt de dades?
- Hi ha un esquema estable (ben estructurat)?
- Quins requisits qualitatius tenim: disponibilitat, escalabilitat, latència, rendiment, consistència…?
Si la resposta està a prop d’una aplicació monousuari, sense connexió amb altres clients, i poques dades, segurament podem gestionar-ho mitjançant persistència en fitxer. Caldria, en tot cas, decidir quin és el format d’aquest fitxer, ja que podem utilitzar solucions existents sense necessitar inventar-nos un format a mida. En POO, utilitzem el concepte de serialització d’objectes: generem una representació del graf d’objectes que permet restaurar-los quan l’aplicació torna a executar-se. El format podria ser a mida, utilitzant JSON, un arxiu de preferències clau-valor, etc.
Si la resposta s’assembla a aplicació multiusuari amb informació compartida per la xarxa i grans volums de dades, estem a prop de necessitar un sistema de gestió de base de dades (SGBD). En aquest cas, parlem de mapatge d’objectes relacional (ORM). Aquest procés consisteix a interposar una capa entre la lògica i la persistència de l’aplicació, de tal forma que podem persistir utilitzant el paradigma d’orientació a objecte en lloc del llenguatge SQL. És un procés complex i no exempte de problemes. A Java es pot implementar a mida (JDBC) o bé utilitzar llibreries de mapatge (JPA).
Els SGBD resolen problemes habituals que ens trobem en el desenvolupament d’aplicacions. Entre ells:
- Definició, creació, manteniment i control d’accés a una base de dades.
- Gestió de transaccions i concurrència (segons el model).
- Facilitats per recuperar dades en cas de danys.
- Gestió d’autoritzacions i accés remot.
- Regles de comportament de les dades en funció de la seva estructura.
Base de dades d’aplicació o d’integració
Una base de dades d’integració és una base de dades que actua com a magatzem de dades de diverses aplicacions i, per tant, integra dades d’aquestes aplicacions.
Una base de dades d’aplicació es controla i accedeix des d’una sola aplicació. Per a compartir dades amb altres aplicacions, l’aplicació que controla la base de dades hauria de proporcionar serveis.
La recomanació general és la d’evitar bases de dades d’integració. En general, les bases de dades d’integració comporten problemes greus perquè la base de dades esdevé un punt d’acoblament entre les aplicacions que hi accedeixen. Generalment es tracta d’un acoblament profund que augmenta significativament el risc que suposa canviar aquestes aplicacions i dificulta la seva evolució.
Model relacional vs NoSQL
Les bases de dades actuals responen, moltes d’elles, al model relacional. Aquest model ha triomfat segurament gràcies a l’establiment d’un estàndard per a la gestió de dades, l’SQL. Existeixen alguns altres models que tenen sentit per a certs nínxols, i que cal considerar: bases de dades en graf, multivalor o orientades a objecte. Però han anat perdent ressò en favor de les bases de dades NoSQL. El creixement d’aquestes es veu afavorit pel Big Data i les aplicacions en temps real. Són sistemes habitualment no estructurats (sense esquema), i poden persistir al costat de solucions relacionals en models de persistència poliglota.
Sense sortir del model relacional, solem tenir extensions sense esquema. Per exemple, camps amb contingut JSON. O taules d’atributs que permeten fer JOINs addicionals. Aquestes extensions ens permeten tenir dades sense esquema dins d’un esquema, tot i que aquestes dades no són tan accessibles des de consultes SQL.
El que cal evitar és tenir esquemes implícits al codi d’accés. Només en pocs casos pot tenir sentit no tenir-ne un esquema:
- Camps a mida imposats per l’usuari
- Objectes sense un tipus uniforme (esdeveniments)
- Pot ser més fàcil fer migracions d’esquemes (implícits)
Punts a considerar a l’hora de decidir-se:
- És una BBDD amb o sense esquema (relacionals vs NoSQL)?
- Existeixen relacions, que utilitzarem per navegar aquesta informació?
- La velocitat és un aspecte crític? Les relacionals sacrifiquen la velocitat en favor de la normalització.
- És important tenir escalabilitat? Les NoSQL escalen millor horitzontalment.
- Les relacionals ofereixen les propietats ACID, mentre les NoSQL són BASE.
Característiques ACID
En el context de bases de dades, ACID (acrònim anglès de Atomicity, Consistency, Isolation, Durability) són tot un seguit de propietats que ha de complir tot sistema de gestió de bases de dades per tal de garantir que les transaccions (operacions sobre les dades) siguin fiables.
Concretament, l’acrònim ACID significa:
- Atomicitat: Una transacció o bé finalitza correctament i confirma o bé no deixa cap rastre de la seva execució.
- Consistència: La concurrència de transaccions no pot produir resultats anòmals.
- Aïllament (o Isolament): Cada transacció del sistema s’ha d’executar com si fos l’única que s’executa en aquell moment en el sistema.
- Durabilitat: Si es confirma una transacció, el resultat d’aquesta ha de ser definitiu i no es pot perdre.
Característiques BASE
Les característiques BASE estan associades a les BBDD NoSQL. Es basen en el teorema CAP (Consistency-Availability-Partition Tolerance), que afirma que és molt difícil tenir més de dues d’aquestes propietats alhora.
Són l’acrònim de:
- Basic Availability: la base de dades funciona la majoria del temps.
- Soft-State: no cal tenir consistència a l’escriptura, ni les rèpliques han de ser consistents.
- Eventual consistency: la consistència es pot tenir més tard en el temps (funcionament mandrós).
Model Relacional
- Model relacional
- Model Entitat-Relació
- Claus primàries (PK)
- Claus externes (FK)
- Formes normals
- Bones pràctiques
- Referències
Model relacional
El model relacional permet a un dissenyador de bases de dades crear una representació lògica i consistent de la informació:
- La informació s’estructura mitjançant taules.
- Cada taula es modela amb diversos atributs.
- Les taules contenen files, que tenen valors per cadascun dels atributs.
- Un possible valor d’un atribut és NULL, el no-valor.
- Les files no poden repetir-se.
- Els atributs que identifiquen una fila conformen la clau primària.
- Les taules es relacionen utilitzant claus externes, que referencien atributs d’altres taules. Es diu que la taula amb la clau externa depèn de l’altra, i se solen anomenar taules filla i pare.
La consistència del model s’aconsegueix utilitzant restriccions (constraints), una forma de restringir el domini d’un atribut o implementar regles de negoci.
Hi ha dos tipus d’integritat al model relacional:
- La integritat de l’entitat: cada fila d’una taula té una clau primària única i no nul·la que l’identifica, o sigui, cada fila representa una única instància d’un tipus d’entitat modelada per la taula.
- La integritat referencial: si el valor d’un atribut referencia el valor d’una altra taula, llavors el valor referenciat ha d’existir.
Les principals restriccions del model relacional són:
- La clau primària (PK): un conjunt d’atributs que identifiquen de forma única una fila. No permeten repeticions.
- La clau externa (FK): un conjunt d’atributs que referencien la clau primària d’una altra taula. No permeten referències a files no existents.
- Els índexs únics: indiquen que un índex no permet que hi hagi elements repetits.
- Les comprovacions (checks): permeten afegir regles per als atributs d’una taula que imposen regles sobre les files.
Model Entitat-Relació
El model entitat relació ens permet modelar el món real utilitzant dos conceptes: les entitats i les relacions:
- Entitats, una cosa que existeix al món real i es pot identificar i distingir de la resta. Són instàncies d’un tipus d’entitat o categoria, és a dir, un valor concret. Tenen atributs que les identifiquen i les descriuen. Són substantius.
- Relacions, que expliquen com es relacionen les entitats. Poden ser verbs (o participis), i representen accions o processos entre entitats. Poden tenir atributs per a afegir informació addicional.
Segons la forma d’identificar una entitat, tenim dos tipus:
- Fortes: no depenen de cap altra, i tenen el seu identificador únic.
- Febles: depenen d’una entitat forta per poder ser identificades. Per tant, el seu identificador inclou el de l’entitat forta i un o més atributs addicionals.
Cardinalitat
Els tres tipus de relacions binàries, segons la cardinalitat, són:
- one-to-one: 1⇔1
- one-to-many: 1⇔N
- many-to-many: M⇔N
I les ternàries:
- one-to-one-to-one: 1⇔1⇔1
- one-to-one-to-many: 1⇔1⇔N
- one-to-many-to-many: 1⇔M⇔N
- many-to-many-to-many: M⇔N⇔P
Generalització i agregació
Algunes entitats poden relacionar-se com a una generalització (is-a):
- Supertipus: un tipus genèric (pare) que té una relació amb un o més subtipus (fills).
- Subtipus: un subgrup d’entitats que comparteixen atributs comuns o relacions diferents d’altres subgrups.
Les PK dels supertipus i els subtipus són les mateixes.
Aquesta relació s’utilitza quan alguns atributs només apliquen a certs subtipus, o bé només existeix la relació per a cert subtipus o supertipus.
En canvi, l’agregació és una relació entre un supertipus i un o més subtipus del tipus part-of. A diferència de la generalització, no hi ha atributs heretats.
Transformació a SQL
Abans d’explicar el procés de transformació a partir del model, Cal explicar el concepte de taula associativa. Les taules associatives són una construcció que permet associar dues o més entitats. Per exemple, resol relacions many-to-many creant dos o més relacions one-to-many.
Per a convertir un diagrama ER en taules podem seguir la següent estratègia:
- Per a transformar les entitats:
- Identificar la clau primària de cada entitat.
- Crear una taula per a cada entitat.
- Si un atribut és una FK, crear la restricció corresponent.
- Per a transformar les relacions binàries:
- Identificar les entitats que participen i la seva cardinalitat.
- Si la relació és many-to-many o té atributs, cal crear una taula associativa.
- En cas contrari no cal crear cap taula, només afegir un FK per cada relació.
- Per a transformar les relacions ternàries, crear una taula associativa i:
- 1⇔1⇔1: una PK amb una parella i dos restriccions unique amb les altres dues parelles.
- 1⇔1⇔N: una PK que inclou l’entitat N i una restricció unique incloent l’altra parella amb N.
- 1⇔M⇔N: una PK amb les entitats M i N.
- M⇔N⇔P: una PK amb les tres entitats.
Claus primàries (PK)
Les files d’una taula tenen atributs.
Una superclau és un conjunt d’atributs que identifica de forma única la fila d’una columna. O sigui, no hi ha més d’una fila amb aquest conjunt d’atributs.
Una superclau no és necessàriament un conjunt mínim. Per exemple, la superclau trivial és la de tots els atributs d’una fila. Si anem traient atributs a la superclau fins que no sigui possible treure’n més, llavors tenim un conjunt mínim, o clau candidata, o simplement clau. Si la clau candidata té més d’un atribut es diu que és composta.
Els atributs d’una clau candidata són els atributs principals. Un atribut que no es troba a cap clau candidata és un atribut no principal.
A una taula pot haver-hi més d’una clau candidata. La clau primària és la clau candidata que s’escull formalment al model relacional per a una certa taula. La resta de claus candidates es diuen claus alternatives.
Una clau pot utilitzar atributs existents al món real, i llavors es diu clau natural. Quan només s’utilitza un atribut com a clau, però no té correspondència fora del model relacional, li diem clau substituta. Habitualment són generades automàticament pel SGBD com seqüències numèriques.
Les claus substitutes tenen pros i contres respecte de les naturals:
- Les naturals poden utilitzar-se per cerques, i no requereixen espai addicional de disc. Però si canvien les especificacions, afecten el disseny. També són més complicades i lentes si tenen més d’un atribut.
- Les substitutes resolen els problemes de les naturals, però tenen els problemes que resolen les naturals. A més, trenquen la 3NF, ja que no tenen cap relació amb la fila, i s’implementen de forma diferent segons el SGBD.
Claus externes (FK)
Una clau externa és un conjunt d’atributs d’una taula que fan referència a la clau primària d’una altra taula. La primera es diu taula filla, i la segona, taula pare.
A un SGBD relacional s’espera que hi hagi integritat referencial: si un atribut o atributs es declaren com a clau externa, només poden contenir NULL o bé referir-se a valors existents de la clau primària de la taula pare.
Quan una fila s’actualitza o s’esborra, el SGDB ha de continuar garantint la integritat referencial. Les accions referencials que es poden definir a un fill són:
- CASCADE: el canvi es transmet des del pare al fill.
- RESTRICT o NO ACTION: no permet el canvi en el pare. Opció per defecte si s’omet, habitualment.
- SET NULL: els valors dels atributs que fan la referència es canvien a NULL.
- SET DEFAULT: els valors dels atributs que fan la referència es canvien al valor per defecte.
Formes normals
La normalització s’aplica al disseny relacional per a poder evitar anomalies quan s’insereix, s’esborra o actualitza una fila.
Les formes normals es comproven de forma incremental: 2NF requereix 1NF, 3NF requereix 2NF. Hi ha més formes normals, però les tres primeres ja permeten evitar els problemes habituals associats a un mal disseny.
1NF
Per complir 1NF, cada atribut d’una taula ha de tenir un sol valor (atòmic). A més, no pot haver grups repetits d’atributs, que són atributs anomenats amb un sufix numèric i amb la mateix funció.
| StudentID | StudentName | Courses | Instructors | InstructorOffices |
|---|---|---|---|---|
| 1 | Alice | CS101, CS102 | Dr. Smith, Dr. Lee | Room 101, Room 102 |
| 2 | Bob | CS101 | Dr. Smith | Room 101 |
Solució: crear una taula amb el conjunt de valors com files.
| StudentID | StudentName | Course | Instructor | InstructorOffice |
|---|---|---|---|---|
| 1 | Alice | CS101 | Dr. Smith | Room 101 |
| 1 | Alice | CS102 | Dr. Lee | Room 102 |
| 2 | Bob | CS101 | Dr. Smith | Room 101 |
2NF
Introduïm el concepte de dependència funcional. Un atribut B és dependent (funcionalment) d’un altre A si a partir d’A obtenim un sol B. A és el determinant i B el depenent, i s’escriu: A ⇨ B.
Això és el que passa habitualment entre una clau i un atribut no principal: l’atribut no principal està determinat per la clau.
La 2NF es dirigeix a claus que tenen més d’un atribut. Per complir-la, cal complir 1NF i, a més, que cada atribut no principal (que no estigui a la clau candidata) depengui funcionalment de tota la clau, no només d’una part.
Solució: si un atribut no principal depèn d’una part, cal moure’l a una taula nova on aparegui només aquesta part.
- student-course table:
| StudentID | CourseID |
|---|---|
| 1 | CS101 |
| 1 | CS102 |
| 2 | CS101 |
- course table:
| CourseID | CourseName | InstructorName | InstructorOffice |
|---|---|---|---|
| CS101 | Intro to CS | Dr. Smith | Room 101 |
| CS102 | Data Structures | Dr. Lee | Room 102 |
- student table:
| StudentID | StudentName |
|---|---|
| 1 | Alice |
| 1 | Bob |
3NF
Introduïm el concepte de dependència transitiva. Si C depèn de B i B de A, llavors C depèn (transitivament) de A. O sigui: si B ⇨ C i A ⇨ B, llavors A ⇨ C.
Per complir 3NF, cal complir 2NF i, a més, que no hi hagi cap atribut no principal que depengui transitivament de la clau primària.
Solució: crear dues taules sense dependències transitives. A cada una hi ha la dependència B de A i a l’altra C de B, respectivament.
- course table:
| CourseID | CourseName | InstructorID |
|---|---|---|
| CS101 | Intro to CS | 1 |
| CS102 | Data Structures | 2 |
- instructor table:
| InstructorID | InstructorName | InstructorOffice |
|---|---|---|
| 1 | Dr. Smith | Room 101 |
| 2 | Dr. Lee | Room 102 |
Bones pràctiques
Sobre com anomenar:
- Utilitzar minúscules i subratllats per a separar paraules.
- Hi ha dues pràctiques per a anomenar taules: utilitzar singular o plural. Preferiblement, noms col·lectius o plurals.
- Els atributs sempre són singulars.
- No passa res si dues taules tenen atributs amb el mateix nom.
- Identificar els atributs que contenen les PK i FK i utilitzar un sufix. Per exemple, nom de la taula més _id.
Sobre integritat:
- Utilitzar sempre restriccions en lloc de fer comprovacions al codi.
- Preferir entitats fortes a febles. Simplifiquen el disseny i generen consultes més òptimes.
- En general, no definir atributs que siguin derivats d’altres.
- Evitar sempre que sigui possible els atributs nullables. Estratègies:
- Utilitzar una relació one-to-one opcional.
- Utilitzar el valor per defecte a la definició de l’atribut.
- Definir com a no nullable aquells atributs que no puguin ser NULL.
- Cal pensar que pot haver-hi múltiples connexions concurrents incidint sobre les mateixes files. Per tant, cal utilitzar transaccions sempre que calgui que un conjunt de comandes es facin totes o cap.
Sobre claus primàries i externes:
- Si la PK no és substituta, millor que sigui immutable o molt estable.
- És millor no implicar molts atributs a la PK. Fa perdre estabilitat. Potser hi ha una clau amb menys camps, o és millor utilitzar una clau substituta.
- Si la PK pot canviar, cal utilitzar ON UPDATE CASCADE al FK per rebre els canvis. Això no cal amb claus substitutes, ja que no canvien.
- Compte amb ON DELETE CASCADE. És preferible esborrar explícitament les files, i que si hi ha un problema d’integritat la restricció no permeti l’operació. Podria tenir sentit utilitzar-ho amb entitats febles.
- ON UPDATE SET NULL/DEFAULT tenen poc sentit, només en tenen pel DELETE, i només si la taula filla és una entitat forta.
- És un problema tenir una FK amb diversos atributs on alguns poden ser NULL. En general, una FK és NULL si qualsevol part ho és.
Sobre optimització, modelar pensant en les consultes que realitzarà l’aplicació sobre la base de dades. Això pot tenir una incidència sobre l’esquema i sobre els índexs, per exemple:
- Cercar sempre sobre camps que estan indexats.
- Afegir índexs sobre els atributs dels joins. No cal per a les PK, s’indexen per defecte.
Referències
- Database normalization
- Database guide
- Surrogate key vs Natural key
- SQL style guide
- The realities of the relational database model
- M2 - Bases de dades
- When (and How) to Use Surrogate Keys
- Database Modeling and Design
Model NoSQL
Model NoSQL
El principal problema d’un model relacional és la distribució d’una BBDD en diferents servidors per raó de mida i rendiment (clústers), i que pugui ser accedida com una sola.
El funcionament en clústers utilitza dues idees:
- La replicació: repliquem les dades en master-slave o peer-to-peer.
- El sharding: situem diferents parts de les dades en diferents servidors.
Les BBDD relacionals no estan dissenyades per a funcionar eficientment en clústers, aquesta és la raó principal per a migrar a NoSQL. La segona és que es veu com una una forma de millorar la productivitat en el desenvolupament d’aplicacions, ja que la interacció amb les dades és més còmoda.
Un model agregat és una col·lecció de dades amb les quals interactuem com a una unitat. Els models agregats permeten treballar més fàcilment amb clústers, agafant l’agregació com a unitat de replicació i sharding. A més, l’agregació també facilita la feina dels desenvolupadors, perquè la manipulació de dades es produeix molt sovint a nivell de agregat.
Els tipus principals de BBDD NoSQL són els models agregats (key-value, document i column) i els de graf.
Key-Value
És la forma d’agregació. Contenen col·leccions de parelles clau-valor, com els diccionaris o les taules hash. La clau és l’identificador i les dades poden ser un objecte JSON, un blob binari o un altre tipus.
| Key | Value |
|---|---|
| “user:1” | { “name”: “Alice”, “age”: 25 } |
| “user:2” | { “name”: “Bob”, “age”: 30 } |
S’utilitzen per a fer caching, gestió de sessions o mètriques senzilles. Són molt fàcils d’implementar, eficients i escalables. Però no permeten fer queries gaire complexes.
Exemples populars: Redis, Amazon DynamoDB (mode key-value).
Document
Les dades s’organitzen el documents de format JSON, BSON o XML. Tenen una estructura jeràrquica, que els atorga flexibilitat. Un document és una unitat atòmica que pot ser consultada, modificada o esborrada. És una entitat que freqüentment té les dades relacionades al mateix document.
Per exemple, aquest seria un usuari amb les ordres associades. No cal predefinir les seves columnes, ni fer queries per trobar les ordres de l’usuari:
{
"_id": "user:1",
"name": "Alice",
"age": 25,
"orders": [
{ "orderId": "A100", "amount": 50.5 },
{ "orderId": "A101", "amount": 25.0 }
]
}
Les queries permeten fer filtratge, agregació o indexació. Per exemple:
- Trobar usuaris més grans de 20:
{ "age": { "$gt": 20 } } - Obtenir una certa ordre:
{ "orders.orderId": "A100" }
Aquest tipus de model permet esquemes flexibles, dades jeràrquiques i també camps indexables per a fer queries més ràpides. Són molt flexibles per no tenir esquema, però l’organització pot portar duplicacions o la impossibilitat de fer relacions (les foreign keys relacionals).
Es poden utilitzar a gestors de continguts, llocs d’e-commerce o per fer event logging.
Exemples populars: MongoDB, Couchbase, Amazon DocumentDB.
Column
Aquest model organitza les dades en columnes i files, però al contrari que el model relacional, les columnes s’agrupen en famílies. Cada familia conté columnes, el que permet optimitzar els patrons d’accés. I cada fila té una clau única.
Exemple:
| Row Key | PersonalInfo | OrderInfo |
|---|---|---|
| 1 | {name: “Alice”, age: 25} | {order1: 50.5} |
| 2 | {name: “Bob”, age: 30} | {order1: 75.0, order2: 25.0} |
S’utilitzen per a emmagatzemar esdeveniments amb timestamps o datasets molt grans amb dades disperses. Permeten operacions d’escriptura intensives, però el modelatge i les queries són difícils d’implementar.
Exemples populars: Apache Cassandra, HBase.
Comparativa
| Funcionalitat | Model Key-Value | Model Document | Model Column-Family |
|---|---|---|---|
| Estructura | Parelles Key-Value | Documents JSON/BSON/XML | Files agrupades en families |
| Consultes | Només per clau | Per camp, consultes niades | Per clau de fila, rang de columna |
| Casos d’ús | Caching, cerques ràpides | Esquemes flexibles, dades jeràrquiques | Anàlisi de dades, series temporals |
| Flexibilitat d’esquema | Senzilla | Alta | Moderada |
Referències
Empresa
- Economia
- Societat
- Sector informàtic
- Proposta de valor
- Estudi de mercat
- Màrqueting
- Estructura legal i pla econòmic
- Pla d’empresa
Economia
Sistema econòmic
El sistema econòmic és l’organització d’una societat per a gestionar els recursos de què disposa. En funció de la regulació del mercat, en tenim dos tipus:
- El capitalisme, que es basa en la propietat privada. La finalitat és satisfer les necessitats humanes amb un mercat desregulat que funciona gràcies a la competència i la cerca del benefici econòmic.
- El socialisme, que es basa en la propietat col·lectiva. La finalitat és aconseguir una societat justa i solidària amb el repartiment de la riquesa mitjançant la planificació i mediació al mercat.
| Aspectes | Capitalisme | Socialisme |
|---|---|---|
| Orígen | Segle XIII | Segle XIX |
| Propietat dels medis de producció | Privada | Social |
| Mecanisme d’assignació | Mercat | Estat |
| Principals factors de producció | Capital | Treball |
| Classes socials | Segons el poder econòmic | No hi ha classes |
| Llibertat de decisió | Existeix libertat | Libertat limitada |
| Treball | Dret | Deure |
| Distribució de la riquesa | Sistema meritocràtic | Sistema igualitari |
| Defensa d’interessos | Individual | Col·lectiva |
| Objectiu | Maximització del benefici econòmic | Maximizació del benestar social |
| Marc institucional | Descentralizació | Centralizació |
El sistema més habitual al món és l’economia mixta, que barreja els dos: tenim un sector privat i un de públic que regula i corregeix el primer en la senda del progrés social. Una conseqüència d’aquest model és l’estat del benestar, on l’estat utilitza part del seu pressupost per a assegurar que tots els ciutadans arriben a un mínim de recursos per a viure dignament.
La regulació del mercat per part de l’estat estableix un marc que pretén protegir l’economia de les seves imperfeccions. Per exemple:
- Quin ha de ser el nivell dels salaris per poder viure dignament.
- Com es resol la falta de competència (monopolis).
- Com es proporcionen els serveis essencials.
- Establir mecanismes justos per a la redistribució de la riquesa.
- Evitar l’enginyeria fiscal per a l’evasió i elusió fiscal.
La doctrina política del neoliberalisme és un corrent amb molta força que vol defensar el capitalisme reduint al màxim la intervenció de l’estat. És el causant de la reducció de l’estat del benestar i les privatitzacions de béns públics dels darrers anys al nostre país.
PIB i creixement
El principi del sistema capitalista és que necessitem créixer perquè funcioni l’economia. El concepte s’anomena creixement econòmic. La tendència és de creixement, però el comportament és cíclic, i ho fa en els anomenats cicles econòmics. Cada cicle té una sèrie de fases: recuperació, expansió, auge, recessió i depressió.
L’indicador més utilitzat per a valorar la situació de l’economia és el PIB (producte interior brut): el valor monetari de tots els béns i serveis finals produïts per una regió durant un any. Es diu que si creix l’economia, llavors ho fan els béns i serveis, i tots ens beneficiem.
Els crítics amb la teoria del creixement econòmic basada en el PIB diuen que:
- Créixer no genera necessàriament cohesió social.
- Provoca agressions al medi, moltes irreversibles.
- Esgota els recursos, que no estaran disponibles a les properes generacions.
- Facilita el mode de vida esclau, on serem més feliços quan més treballem, més guanyem i més consumim.
A més, els pilars del sistema econòmic capitalista es basen en principis poc ètics:
- La primacia de la publicitat, que ens obliga a comprar el que no necessitem.
- El crèdit, que ens permet obtenir recursos pel que no necessitem.
- La caducitat dels productes (obsolescència programada).
Els defensors del creixement afirmen que el mateix sistema, amb el mecanisme de la competència, s’encarregarà de resoldre els problemes que genera. Per exemple, amb solucionisme tecnològic.
També hi ha alternatives de responsabilitat social, que es refereixen a creixement sostenible per expressar que és possible créixer però fer-ho en favor el medi i el benestar de la societat. Aquestes plantegen substituir el PIB com a indicador, però encara no hi ha un consens:
- Índex de desenvolupament humà (IDH).
- Felicitat interna bruta (FIB).
- Índex de benestar canadenc (IBC).
- Renda nacional bruta modificada (RNB*).
- Índex de progrés real (IPR).
La teoria del decreixement és un moviment polític, econòmic i social favorable a la disminució regular controlada de la producció econòmica, amb l’objectiu d’establir una nova relació entre l’ésser humà i la natura.
Els principis d’actuació d’aquesta teoria segons Serge Latouche són:
- Reavaluar els valors individualistes i consumistes i substituir-los per ideals de cooperació.
- Reconceptualitzar l’estil de vida actual.
- Reestructurar els sistemes de producció i les relacions socials en funció de la nova escala de valors.
- Relocalitzar: es pretén reduir l’impacte generat pel transport intercontinental de mercaderies i se simplifica la gestió local de la producció.
- Redistribuir la riquesa.
- Reduir el consum, simplificar l’estil de vida dels ciutadans. El decreixement aposta per una tornada al petit i al que és simple, a aquelles eines i tècniques adaptades a les necessitats d’ús, fàcils d’entendre, intercanviables i modificables.
- Reutilitzar i reciclar: allargar el temps de vida dels productes per evitar el malbaratament. Evitar el disseny de productes obsolescents.
Emprenedoria
Autoocupació
L’autoocupació permet a una persona obtenir ingressos mitjançant una activitat que crea per a un mateix, en lloc de treballar per una altra persona o organització (el treball per compte d’altri). Bàsicament tenim dues opcions:
- Ser treballador independent. Associat als conceptes de treballador autònom o freelance.
- Crear una empresa capitalista, és a dir, una organització de dues o més persones que busca un benefici econòmic mitjançant el desenvolupament d’una activitat. Una empresa (societat) pertany als socis capitalistes. Quan hi ha beneficis, es poden reinvertir en l’empresa o bé repartir dividends entre els socis capitalistes.
L’emprenedoria és la iniciativa d’un emprenedor de portar a la pràctica una idea de negoci, és a dir, crear una empresa i dur a terme la producció del bé o prestació de servei. El coneixement, relacions i capacitats de l’equip són essencials a l’hora de definir una bona idea. Es parla d’intraprenedor en referència als emprenedors que exerceixen com a tals en empreses de les quals no són titulars. Finalment, l’empresari (pot ser o no el mateix l’emprenedor) és qui després dirigeix i gestiona l’empresa.
Un emprenedor haurà de prendre decisions, assumir riscos, ser creatiu, constant, tenir confiança en si mateix, sentit pràctic, saber organitzar-se i tenir facilitat per a les relacions personals. Pot tenir diferents motivacions, com econòmiques, socials o tecnològiques.
Visió, missió i valors
Una eina per definir l’estratègia per a un emprenedor és la de la visió, la missió i els valors. Tot parteix de fer explícita la seva visió:
- Què volem ser i on volem arribar?
- Com ens enfrontarem al canvi?
- Al mercat només podem accedir amb un avantatge competitiu. Quin és i com el podrem mantenir?
Aquesta visió ha de ser clara, concisa, memorable i ha de definir un futur desitjable.
Per altra banda, la missió és la raó d’existir de l’organització, i respon a les preguntes: què fem, per a qui, quines necessitats satisfem, i com ens diferenciem.
Finalment, els valors ens indiquen quines són les nostres formes particulars per seguir el camí que ens porta a la visió. Proporcionen un criteri en el moment de prendre decisions incertes, que en alguns casos podrien ser una solució fàcil però que portarien a trair els nostres valors.
Estratègia de negoci
En els negocis, un avantatge competitiu és un atribut que permet a una organització superar els seus competidors.
Una eina per a determinar l’avantatge competitiu d’una empresa és la cadena de valor, que permet examinar i dividir l’empresa en les seves activitats estratègiques més rellevants i entendre els costos, les fonts i la seva diferenciació.
Tenim dos tipus d’activitats a la cadena de valor:
- Les primàries o competències distintives (core business), enfocades a l’elaboració del producte o servei i la seva venda: logística interna i externa, operació (producció), màrqueting i vendes, servei post-venda.
- Les de suport: infraestructura, recursos humans, recerca i desenvolupament (R+D) i compres.
En el context de la globalització, quan una empresa pot desenvolupar activitats en altres llocs geogràfics, se’n parla de cadena global de valor.
Els avantatges competitius d’una activitat econòmica poden classificar-se en tres:
- Lideratge en costos amb qualitats similars. Es pot fer a diferents punts de la cadena de valor.
- Diferenciació d’un producte o servei de certa complexitat segons la combinació de la seva qualitat i les seves característiques.
- Segmentació de mercat segons variables geogràfiques, personals, psicogràfiques o de comportament.
Aquestes són algunes estratègies que poden diferenciar-nos dels nostres competidors:
- Visibilitzar l’equip, destacant l’experiència, fiabilitat i capacitat de resoldre problemes complexos.
- Bona experiència de client amb un servei excel·lent, comunicació clara i freqüent i transparència.
- Amplia distribució i accés als serveis, associant-se amb proveïdors o utilitzant plataformes i mercats en línia.
- Alta eficiència operativa utilitzant metodologies àgils i automatitzant tasques rutinàries.
- Construcció de la marca per a obtenir una sòlida reputació, destacant èxits i satisfacció de clients.
- Desenvolupar relacions de llarg termini amb els clients, oferint suport, manteniment i actualitzacions constants.
- Agilitat en el sector, mantenint-se al dia de tendències, metodologies i tecnologies emergents.
- Iniciatives de responsabilitat social que incloguin les comunitats locals, proveïdors sostenibles i ètics, pràctiques que redueixen la petjada de carboni, transparència operativa, economia circular, treball cooperatiu o educació dels clients.
Finalment, cal comunicar-ho mitjançant una estratègia go-to-market: que la nostra marca arribi als potencials clients i puguin distinguir els nostres productes o serveis de la resta mitjançant el seu possicionament.
El model de negoci d’una empresa descriu el tipus de negoci dins del context del mercat, a qui va dirigit, com es vendrà i com s’aconseguiran els ingressos. Per exemple, alguns tipus de models són la fabricació, la distribució, el retail, l’e-commerce, la publicitat, etc.
Una eina molt pràctica per a definir el model de negoci és el Canvas.
Un cop tenim clar el nostre model de negoci, podem concretar-lo i descriure’l al nostre pla d’empresa. És un document descriptiu amb els passos i els números concrets en que es basa la nostra iniciativa, i que ens pot servir per presentar la nostra idea davant dels possibles inversors.
Innovació
L’estratègia més adient per a ser competitiu és la innovació. Innovar és introduir alguna cosa nova o diferent. Fer-ho en un mercat global i dinàmic comporta risc: podem tenir una gran idea, però si no crea valor, no tindrà impacte.
La innovació ens permet:
- Diferenciar-nos, l’avantatge competitiu essencial.
- Millorar la productivitat, i per tant els costos.
- Tenir una certa cultura d’empresa d’innovació contínua.
Les innovacions poden estar dirigides cap a:
- El producte o servei: quines són les característiques.
- El procés: com es gestiona la producció.
- El màrqueting o disseny: quin és el mètode de comercialització i com es concreta al preu, la promoció i la distribució.
- La tecnologia: innovació derivada de la recerca i desenvolupament tecnològic.
- L’organització: quines persones i com estructuren l’empresa.
Quines serien les característiques d’un equip innovador?
- Divers: com ho són les habilitats i responsabilitats necessàries.
- Inquiet: el procés innovador es continu, cal actualitzar-se i estar atent.
- Amb una bona formació en relació a l’àmbit o sector de l’empresa.
Finalment, l’equip humà ha de tenir un pla. El pla d’empresa és un document formal que principalment defineix els objectius del negoci, el mètodes per a aconseguir-los i la seva planificació.
Recursos per a l’emprenedoria
Sabadell
- El Vapor Llonch fa formació, programes d’ocupació, orientació professional, borsa de treball i creació i consolidació d’empreses. També té un observatori de l’economia local.
- L’oficina d’atenció a l’empresa i l’autònom/a de Sabadell es un PAE, que permet crear una societat. També té un centre de promoció empresarial amb oficines i coworking.
- La Cambra de Sabadell disposa de diferents serveis amb cost associat. Tenen un programa d’acceleració de startups innovadores.
Catalunya
- ACCIÓ és l’agencia per la competitivitat de l’empresa de la Generalitat. Està orientada a millorar la competitivitat de l’empresa, oferint una sèrie de serveis per a l’emprenedoria. Té una secció específica per al sector TIC.
- El Canal Empresa de la Generalitat té una sèrie de serveis sobre finançament, innovació, sostenibilitat, etc. També informa sobre la constitució i tràmits per a crear una empresa.
- Catalunya Emprèn.
- Xarxa Emprèn.
- Hi ha un portal d’economia social al departament de treball de la Generalitat. Des d’aquest, es pot gestionar la creació d’una cooperativa o societat laboral.
Estat
El CIRCE és el Centro de Información y Red de Creación de Empresas, un sistema que permet fer els tràmits de creació de societats en línia. Per a crear una empresa de tipus Societat Limitada:
- Anar a un Punt d’Atenció a l’Emprenedor (PAE), on se li assessorarà en tot el relacionat amb la definició del seu projecte empresarial i se li permetrà iniciar els tràmits de constitució de l’empresa.
- Iniciar els tràmits omplint el DUE a través del portal CIRCE. Per a això és necessari disposar d’un certificat electrònic.
Finançament
Alguns tipus de finançament que es poden considerar:
- Ajuts i subvencions
- Premis, concursos i beques
- Capitalització de l’atur
- Microcrèdits
- Fundraising o mecenatge
- Inversors privats (business angels)
- Crowdfunding o micromecenatge
- Banca ètica i coooperatives de crèdit
- Incubadores i acceleradores
Alguns enllaços interessants:
- ACCIÓ, cercador d’ajuts i serveis per a l’empresa.
- ACCIÓ, servei d’assessorament financer per a startups.
- Startup Capital, ajut per a startups tecnologiques.
- Business angels per a emprenedors.
- Finançament i gestió econòmica del Canal Empresa.
- Ajuts i subvencions de l’ajuntament de Sabadell.
Referències
- Ventaja competitiva
- Importance of innovation
- Competitive advantage
- Cómo funciona la máquina económica
Societat
- Entorn empresarial
- Impacte social i ambiental
- Responsabilitat social
- Objectius de Desenvolupament Sostenible (ODS)
- Les cooperatives
- Referències
Entorn empresarial
L’activitat d’una empresa requereix d’una sèrie d’àrees funcionals. Algunes, relacionades directament amb el mercat o competències clau:
- Direcció i control
- Compres a proveidors i emmagatzametge
- Producció
- Comercial o vendes
D’altres, són de suport a les primeres:
- Recursos humans
- Àrea financera (obtenció de recursos econòmics)
- Comptabilitat
- Administració
L’entorn d’una empresa inclou proveïdors, clients, competidors, entitats financeres, administracions públiques, mercat laboral i comunitat.
Cal mencionar el concepte de stakeholder o grups d’interès, persones i grups a què afecta l’activitat de la nostra empresa i que poden influir en el seu funcionament. Per tant, cal tenir-los en compte en les decisions de l’empresa. Principalment inclouen els accionistes i els treballadors, però també proveïdors, clients, consumidors, entitats reguladores i competència.
La empresa responsable ha de dirigir-se estratègicament no només a satisfer als shareholders o accionistes, que busquen el profit de les seves inversions, sinó també dels stakeholders, que componen el teixit social a que afecta l’activitat econòmica.
Impacte social i ambiental
L’activitat empresarial té impactes econòmics, socials i ambientals. Pel fet d’integrar-se a la societat i interactuar amb ella, els comportaments associats poden produir impactes positius i negatius.
Impactes positius:
- Suministre de productes o serveis útils.
- Desenvolupament humà: creen ocupació, formen persones.
- Creació de riquesa, que distribueix entre treballadors, administració (impostos), accionistes i proveidors.
- Les institucions que ordenen l’activitat econòmica busquen el bé comú, i per tant les empreses ho fan subsidiàriament.
Impactes negatius:
- Enfermetats professionals i accidents laborals.
- Reconversió o desaparició de tipus de feines.
- Per als treballadors, productes insegurs o insans.
- Per als consumidors, productes adulterats, insegurs, tòxics, caducats, abusos amb la privacitat de les dades.
- Deteriorament ecològic per contaminació, consum de recursos no renovables, sobreexplotació de recursos renovables, amenaces a la biodiversitat.
A les resposabilitats d’una empresa es poden distingir tres nivells:
- Legal, en referència al cumpliment de la legislació civil, administrativa i penal.
- Ètica, segons la deontologia professional (professió) o cultura corporativa (empresa).
- Moral, en relació al conjunt de regles o principis de comportament d’una persona o col.lectivitat pròpies d’una cultura.
Si el comportament empresarial té una regla clara, n’hi ha prou amb aplicar-la. Si no, podem aplicar el següent enfoc: el correcte és el que produeix més beneficis per al major nombre de persones, i que no viola els drets individuals.
Podem qualificar els comportaments empresarials com a:
- Inmorals: quan només busquem si la acció té beneficis o èxit. Les lleis són obstacles a superar, i no hi ha conducta ètica.
- Amorals: busquem els beneficis o èxit dins de les regles del mercat i la llei. S’interpreta que l’ètica no és un tema empresarial, o no es considera rellevant l’efecte de les accions.
- Morals: es busca l’èxit però només dins dels preceptes de conducta acceptats per la societat. L’empresa té objectius ètics, i interpreta l’esperit de la llei.
Per a l’impacte ambiental, hi ha una tendència a afegir legislació per part dels organismes públics que obliga a les empreses afectades. La difusió de la informació de l’impacte social d’una empresa és voluntària per a una empresa convencional. Aquesta informació permetria als individus prendre decisions de consum i a les empreses i l’administració, decisions de contractació.
Responsabilitat social
La responsabilitat social corporativa o empresarial (RSC/RSE) és el compromís que de forma voluntària assumeixen les empreses i organitzacions per fer-se responsables dels seus impactes, fomentar un desenvolupament sostenible i crear valor econòmic i social.
Els àmbits on calen accions transformadores són els següents:
- Bon govern
- Gestionar amb ètica i transparència.
- Contribuir al benestar de la societat i al bé comú.
- Mesurar la gestió dels àmbits per poder prendre decisions.
- Informar públicament dels avenços.
- Fomentar espais de diàleg amb els grups d’interés.
- Econòmic
- Evitar l’evasió i elusió fiscal.
- Apostar per la compra de proximitat i els recursos locals.
- Afegir criteris no econòmics a les compres.
- Promoure relacions basades en el benefici mutu a la cadena de proveïment.
- Innovar en processos, productes i serveis i invertir en R+D+I.
- Realitzar accions d’inversió socialment responsable.
- Laboral
- Generar ocupació de qualitat: estabilitat i condicions de treball dignes.
- Gestionar les persones amb la seva participació quan els incumbeix.
- Assegurar entorns de treball saludables.
- Desenvolupar les competències i habilitats dels treballadors.
- Gestionar positivament la diversitat.
- Fomentar la reforma horària i la conciliació.
- Ambiental
- Reduir els consums de recursos naturals i energètics.
- Promoure una mobilitat sostenible.
- Reduir i reaprofitar residus.
- Utilitzar productes i serveis respectuosos amb el medi.
- Sumar esforços destinats a la mitigació i l’adaptació al canvi climàtic.
- Apostar per l’economia circular.
- Social
- Impulsar la cohesió social i el compromís amb la comunitat.
- Promoure el consum responsable.
- Integrar la responsabilitat social en l’educació, la formació i la recerca.
- Respectar i protegir els drets humans en tota la cadena de valor.
- Intercanviar experiències i bones pràctiques.
Hi ha una sèrie d’eines de gestió de la responsabilitat social per a PIMES associades a certificacions que permeten avaluar una organització i obtenir un segell reconeixible socialment. En aquesta llista destaquen el Balanç Social o el Pacte Mundial.
Pel fet que cada cop és més important aparèixer social com a empresa responsable socialment, s’està produint el fenòmen del “rentat d’imatge verd” (greenwashing): és l’acció d’una empresa, un govern o un organisme d’usar el màrqueting per a promoure la percepció que els seus productes, objectius o polítiques són respectuosos amb el medi ambient, quan en realitat funciona de manera oposada.
Objectius de Desenvolupament Sostenible (ODS)
Els ODS és una iniciativa de les Nacions Unides amb una agenda fins al 2030. Són una crida universal a l’acció per posar fi a la pobresa, protegir el planeta i millorar les vides i les perspectives de les persones a tot el món.
Estan dirigits a la societat, de forma general. Aquesta és l’aproximació empresarial que fan les Nacions Unides als ODS per a cada objectiu:
- Fi de la pobresa. Donar oportunitats laborables per a grups vulnerables, amb condicions dignes i impactant positivament a les comunitats locals.
- Fam zero. Investigar la tecnologia agrícola, pràctiques sostenibles a la cadena de subministrament i accés a aliments sans i suficients.
- Salut i benestar. Plans de seguretat i salut laboral per als treballadors i cadenes de valor, evitant impacte negatiu de les seves operacions i contribuir positivament sobre el benestar.
- Educació de qualitat. Formació dels empleats i grups d’interés, inversió en educació per a millorar les oportunitats laborals i salaris.
- Igualtat de gènere. Garantir els mateixos drets i oportunitats laborals a la dona, programes d’empoderament econòmic.
- Aigua neta i sanejament. Gestió sostenible de recursos hídrics en la elaboració de productes i serveis, foment de la millora de la gestió sostenible en la cadena de valor.
- Energia asequible i no contaminant. Inversió en fonts d’energia neta, tecnologies que redueixen el consum elèctric, projectes per electrificar comunitats desfavorides.
- Feina decent i creciment econòmic sostenible. Garantir les condicions dignes de treball, foment de la ma d’obra vulnerable.
- Industria, innovació i infrastructura. Innovació per a la sostenibilitat, processos industrials sense impacte sobre el medi, infrastructures sostenibles i resilients, accés TIC a tots els treballadors, tecnologies eficients i sostenibles.
- Reducció de les desigualtats. Condicions laborals dignes, redistribució igualitària dels salaris, mecanismes per evita l’evasió fiscal, projectes de cooperació al desenvolupament.
- Ciutats i comunitats sostenibles. Innovació per al desenvolupament de ciutats sostenibles i intel·ligents, mobilitat sostenible, reducció de consum energètic i aigua.
- Producció i consum responsables. Ús eficient de recursos a la cadena de valor, retirar productes i serveis amb consum excessiu, impuls d’energies renovables, reutilització hídrica, reduir la contaminació, formació en pràctiques de producció i consum sostenible, combatre el desperdici alimentari, ecoetiquetat.
- Acció per al clima. Reduir emissions de gasos, impulsar energies renovables, innovació al medi.
- Vida submarina. Reduir la contaminació de mars i oceans, promoure la pesca sostenible, ajustar-se al dret internacional.
- Vida d’econsistemes terrestres. Evitar l’impacte sobre els econsistemes i hàbitats en les operacions de l’empresa, respectar la normativa corresponent, integrar la conservació de la diversitat biològica.
- Pau, justícia i institucions sòlides. Incorporar el respecte als DDHH i transparència en l’organització, evitar quasevol tipus de violència sobre menos i grups volnerables, impulsar l’estat de dret.
- Aliances per a assolir els objectius. Aliar-se amb el sector públic, la societat civil, universitats i altres empreses per a realitzar projectes en pro dels ODS.
Les cooperatives
La organització amb responsabilitat social per antonomasia té la forma jurídica de cooperativa. Entre els principis que identifiquen la cooperativa cal destacar:
- La democràcia empresarial que defineix la seva gestió.
- La participació econòmica del socis.
- L’interès per proporcionar formació i informació als socis.
- La millora de la situació econòmica i social, tant dels components com de l’entorn comunitari.
Les cooperatives poden ser de diversos tipus. Les més addients per a produir béns o serveis són les cooperatives de treball associat. També hi ha les cooperatives de serveis per a professionals per compte propi. Finalment, hi ha dues condicions que poden tenir les cooperatives: ser d’iniciativa social o ser sense ànim de lucre. En funció d’aquestes característiques, poden tenir beneficis concrets.
Referències
- Economia social
- La responsabilidad social de la empresa
- Responsabilitat social
- Recursos formatius de responsabilitat social
- El sector privado ante los ODS
- 17 objectius per a les persones i el planeta
- Carlos Taibo: “El planeta se nos va y es necesario frenar de inmediato la locomotora del crecimiento”
- ¿Es el decrecimiento económico una alternativa real?
- Los fallos del PIB y sus alternativas
L’empresa informàtica
- Software a la economia
- Estratègia de negoci
- Deute tècnic
- Model de negoci
- Impacte social
- Impacte ambiental
- Referències
Software a la economia
El software és omnipresent:
- Substituint negocis tradicionals, com llibreries, publicitaris, música, telecomunicacions, selecció de personal, serveis financers, etc.
- Menjant-se la cadena de valor de diferents negocis, tot i no substituir-los, com a la fabricació de cotxes, la carrera espacial, la logística i la distribució, etc.
El progrés humà podria veure’s com a quatre revolucions industrials:
- El vapor, l’aigua i la producció mecànica.
- La divisió del treball, l’electricitat i la producció massiva.
- L’electrònica, la informàtica i la producció automatitzada.
- L’anàlisi de dades, els dispositius mòbils, la intel·ligència artificial, el machine learning, la robòtica i la genòmica.
No sempre podem fer investigació des de l’empresa. Però l’ús de les tecnologies de propòsit general ens dona un marc d’innovació que podem integrar en el nostre negoci.
La tecnologia ha transformat els negocis i també la societat, produint canvis disruptius a cadascuna d’aquestes revolucions que han afectat les persones i les seves feines.
Estratègia de negoci
La innovació permet a l’empresa entrar al mercat i adaptar-se als canvis. L’empresa software ho pot fer de diverses maneres:
- Fent la creativitat un hàbit (cultura corporativa).
- Tenir un cicle de desenvolupament (SDLC) àgil.
- Tenir un espai de treball (físic o virtual) funcional, flexible i mòbil. Tenim eines de gestió de codi i DevOps al nostre abast.
Què necessita el món empresarial del software?
- Hi ha negocis completament software:
- Creació, adaptació, ampliació i manteniment d’aplicacions off-the-shelf i a mida.
- Sharing economy.
- Software as a Service.
- Tenim la tasca de la digitalització d’empreses:
- Presència a internet (web, xarxes).
- Estratègia de comunicació i màrqueting.
- Venda online.
- Digitalització dels processos de gestió (CRM, ERP, treball col·laboratiu, teletreball).
- Tenim feina a les activitats de la cadena de valor:
- Serveis dins de les empreses, especialment en automatització, qualitat, anàlisi de dades i business intelligence.
- Interacció entre empreses (B2B), com adaptació de protocols, API Rest.
La nostra estratègia go-to-market hauria d’incloure:
- La visibilitat d’un equip innovador i ben format.
- El nostre portfolio de solucions i creativitat.
- Solucions sensibles a l’èxit del client a l’estructura de costos (per ús).
- Bona comunicació escrita en les propostes tècniques, justificades i amb detall.
- Codi font lliurat al client amb la promesa que podrà ser assumit per qualsevol altre proveidor.
Deute tècnic
El deute tècnic és el resultat de decisions tècniques dolentes o subòptimes que finalment generen problemes per a mantenir i expandir una solució. Com a integrants de l’economia del software, tenim una responsabilitat en reduir aquest deute.
La raó del deute tècnic pot ser:
- El resultat d’una decisió basada en paràmetres de negoci i no sostenible.
- El resultat de fer un mal disseny i la seva implementació del codi.
En ambdós casos, el resultat és el mateix: un codi que no s’entén, que no és robust, que està mal documentat, que provoca infelicitat als programadors i que molt difícilment es pot modificar o estendre.
Què podem fer per reduir el deute?
- Tenir clara l’estratègia tècnica inicial. Fer una bona avaluació de les possibilitats, i fer-ho amb un objectiu ben definit.
- Tenir un responsable d’arquitectura.
- Dissenyar amb la seguretat com a requisit.
- Pensar com es podrien substituir els components o llibreries de la nostra solució.
- Mantenir la refactorització a petita escala dins del procés, millorant el codi.
- Tenir testos de regressió automatitzats.
- Millorar la cobertura dels tests.
- Reescriure codi, si cal. És un moviment perillós, però de vegades necessari.
- Fer una bona documentació.
- Tenir bons canals de comunicació per a gestionar problemes.
Model de negoci
El nostre model de negoci software ha de descriure el tipus de negoci dins del context del mercat, a qui va dirigit i com s’aconseguiran els ingressos necessaris.
El model de negoci descriu com fa una organització per a crear, lliurar i capturar valor en un context econòmic, social o cultural.
Si utilitzem el canvas com a eina per a definir el model de negoci, podem trobar els següents aspectes.
Segments de clients
Podem caracteritzar els client segons una sèrie de paràmetres.
Segons l’audiència:
- B2B (venda a altres empreses)
- B2C (venda a usuari final)
- C2C (marketplace).
Segons l’àmbit del producte:
- Off-the-shelf (“caixa”): generalista, solució global. Pot permetre customització o implementar personalització.
- A mida: fet a mida, per a un problema concret del client.
- Híbrid: producte off-the-shelf que permet desenvolupaments a mida gràcies a un API o similar. Pot ser obert o tancat.
Segons la plataforma destí:
- Per a una o diverses plataformes: Android, iOS, etc.
- Plataforma agnòstica. Habitualment basada en web.
Segons la interacció dels usuaris:
- One-to-many (clients)
- Many-to-many (usuaris productors i usuaris consumidors).
Segons el model de llicències:
- Propietari
- Open-source
Proposta de valor
Explicar la proposta de valor: una afirmació que identifica els beneficis clars, mesurables i demostrables que els clients obtenen en comprar cadascun dels productes o serveis. Caldria explicar-ho per cada segment.
En software significa explicar almenys les funcionalitats, el rendiment, l’arquitectura i el model de suport.
Canals
Fases:
- Coneixement: web, màrqueting online.
- Avaluació: freemium, trial.
- Compra: contracte, e-commerce, store.
- Lliurament: on-premise vs off-premise (cloud) vs híbrid.
- Suport: issue/bug tracker, CRM.
Segons el lliurament:
- On-premise: el software s’instal.la i funciona a les instal.lacions del client.
- Cloud-based: el software funciona al núvol o a un proveïdor de allotjament (SaaS).
- Híbrid: barreja els dos anteriors. Hi ha instal.lació, però també es compta amb el núvol per al seu funcionament.
Relació amb els clients
Segons el model de negoci. Podem utilitzar eines CRM integrades en el nostre ERP, comunitats a les xarxes, auto-servei.
Ingressos
Podem tenir un o més fluxos:
- Aplicacions de pagament. Els clients paguen per instal·lar un producte.
- Publicitat a l’aplicació. L’aplicació és gratuïta, però veneu llocs d’aplicacions per a publicitat.
- Compres des de l’aplicació. L’aplicació és gratuïta, però guanyes venent productes o serveis mitjançant una aplicació.
- Subscripcions. Els usuaris paguen anualment o mensualment una quota de subscripció.
- Model d’ingressos de programari basat en l’ús. Els clients paguen només pel que fan servir.
- Desenvolupaments a mida. Els clients paguen per desenvolupar funcionalitats a mida.
- Càrrecs per suport, serveis empresarials i consultoria.
Activitats clau
- Programació.
- Suport i consultoria.
- Anàlisi, venda o accés a dades.
- Optimització interna i innovació.
- Formació contínua.
Recursos clau
- El nostre codi, que pot ser propietari o open source, i llibreries de tercers.
- Les nostres dades (com a actius).
- Programadors que donen valor a l’empresa.
- Ordinadors.
- Local físic vs teletreball.
- Subscripcions a serveis o comptes de desenvolupament.
Costos
Revisar els costos dels recursos clau.
Socis clau
- Dependència d’empreses tecnològiques.
- Store amb les seves normes i tecnologies.
- APIs de tercers.
Impacte social
Els codis deontològics que hi ha a les referències d’aquest document ens donen una visió general de l’impacte de la professió de desenvolupament de software. La responsabilitat podria veure’s en tres àrees: negoci, tecnologia i societat.
Negoci
La pregunta a fer-se és: segueixo l’estratègia correcta? Aquesta estratègia ha de ser compatible a la de les altres dues àrees esmentades.
Hem d’assegurar-nos de complir tota la llei i normativa en relació a la societat. En particular:
- El reglament general de protecció de dades europeu (RGPD).
- La llei orgànica de protecció de dades personals i garantia dels drets digitals, que compatibilitza la legislació espanyola amb el RGPD.
- La llei de serveis de la societat de la informació i del comerç electrònic.
Tecnologia
Ens hem de preguntar: utilitzo l’eina correcta per la feina? És quelcom nou o reinventem la roda? Estic generant deute tècnic? Aquestes decisions impactaran al mercat del desenvolupament software, vist com un col.lectiu que interactua i acaba reprenent o col.laborant en solucions tècniques.
Societat
Ens preguntarem: millorem les vides de les persones? Pot utilitzar-se de forma nociva? Exclou persones? És ètic? És segur? A qui pertanyen les dades?
En aquesta àrea ens podem preguntar:
- Si utilitzem les eines i solucions més responsables socialment, com per exemple, el codi obert.
- Si respectem el dret a la privatitat en el tractament de les dades de les nostres solucions.
- Si seguim les línies universals de l’IA.
Impacte ambiental
Computació verda: ús eficient dels recursos informàtics, minimitzant l’impacte ambiental. Aproximacions:
- Longevitat dels productes, ja que el procés de fabricació és la part més significativa de l’ús de recursos naturals.
- Disseny eficient dels data centers.
- Eficiència del software (algorismes, assignació de recursos, virtualització, servidors de terminals).
- Gestió de energia amb l’ús de components no utilitzats, reduir voltatges, parar màquines, fonts d’energia més eficients, etc.
- Reciclatge d’equips per ser reutilitzats.
- Cloud computing (virtualització) versus edge computing (més a prop).
- Teletreball, reduint el transport.
En particular, programació verda:
- Minimitzar l’emissió de CO2.
- Dissenyar les aplicacions eficientment per reduir el consum d’energia.
- Consumir electricitat amb la intensitat de CO2 mínima (mix de fonts).
- Construir aplicacions que siguin eficients amb el hardware, estenent la seva vida.
- Maximitzar l’eficiència energètica del hardware, reduïnt el nombre de servidors amb la major ràtio d’utilització.
- Reduit la mida i la distància recorreguda de les dades per la xarxa.
- Construir aplicacions conscients del CO2, que permetin gestionar la demanda i moure-la a regions o moments de menys intensitat.
- Per poder optimitzar, mesurar el CO2, l’energia, el cost, l’ús de xarxa i el rendiment.
Referències
- Why software is eating the world
- Social Responsibility Impacts Software Development Processes
- How to recognize exclusin in AI
- Green Software Foundation
- Best Practices of Sustainable Software Development
- Codi Deontològic
- Código Ético y Deontológico de la Ingeniería Informática
- Programming ethics
Proposta de valor
- Punt de partida
- La idea de negoci
- Generació d’idees
- Proposta de valor
- Canvas del model de negoci
- Pla estratègic
- Referències
Punt de partida
Autoocupació: activitat professional o empresarial generada per una persona, i que l’exerceix de forma directa pel seu compte i risc.
Qualitats de les persones emprenedores:
- Creativitat
- Iniciativa
- Responsabilitat
- Autonomia
- Assumpció de riscos
- Competències socials
- Competències personals
Intraemprenedoria: són treballadors que des del seu lloc de treball a una empresa on no són propietaris desenvolupen i posen en pràctica les seves qualitats emprenedores en benefici de l’empresa per a la que treballen.
La idea de negoci
La idea és el producte o servei que es pretén oferir al mercat.
Cal avaluar-la i veure la seva viabilitat:
- és útil
- es diferencia de la competència, creant valor afegit
- genera innovació: és nou, millora un producte/servei existent o el seu procés de fabricació
- és rendible
Creativitat i innovació
Origen de la idea innovadora:
- necessitat no satisfeta
- factor diferenciador
- innovació en tecnologia
- aprofitar la pròpia formació o experiència
- repetir experiències alienes
- cercar referències en internet
Podem validar la idea amb preguntes clau:
- quin valor aporta la nostra idea o producte?
- què necessiten els meus clients potencials?
- com és el sector on vull emprendre?
- quin segment de clientes és rellevant per a nosaltres?
- com puc testar la validesa de la meva idea?
- quin tipus de comunicació volem establir amb el nostre entorn?
- què esperem aportar a la societat?
- quins canals de distribució i promoció són rellevants?
Innovació i desenvolupament econòmic
Evolució del factor generador de riquesa i desenvolupament a la història:
- la terra dedicada a l’agricultura
- amb la revolució industrial, els recursos energètics
- actualment, el coneixement i la innovació
Factors importants:
- I+D+I: investigació, desenvolupament i innovació.
- el desenvolupament integrat: econòmic, social i sostenible.
Com podem innovar
Podem innovar:
- en el producte (total o evolució)
- en el procés (despeses de producció i distribució, millora de la qualitat)
- en màrqueting (disseny, envasat, posicionament, promoció)
- en l’organització de l’empresa (canvis en les pràctiques i procediments o al lloc de treball)
Valoració de la idea de negoci
Un possible esquema per validar seria:
- fer un estudi inicial: possibilitats que ofereix el mercat, forats que hi ha, cercar fonts, imaginació
- fase de consulta: cercar recolzament d’experts, de possibles proveïdors i clients, normativa vigent
- anàlisi de l’acollida: en funció de com de positiva hagi estat, veure si cal reconsiderar la idea o descartar-la
- presa de decisions: decidir què farem del projecte
- posada en pràctica: implementar-la
Generació d’idees
Brainstorming: la Tempesta d’Idees.
Scamper: sobre un element que es desitja millorar, buscar idees tenint en compte les preguntes derivades:
- Substituir, Combinar, Adaptar, Modificar, Buscar altres usos, Eliminar, Canviar la forma.
Sinèctica: Generació d’idees per analogia.
Pensament lateral: No sempre hem de pensar de forma lògica. Podem fer un enfocament indirecte i creatiu.
Els sis barrets (Six thinkings hats): Analitzar el problema fent ús de sis tipus de pensament diferents:
- objectiu (fets, números, verificació)
- intuïtiu (emocions, sentiments estètics)
- creatiu (alternatives, atzar, extrems)
- negatiu (riscos, perills, imperfeccions)
- positiu (optimisme, futur, somnis)
- control (síntesi, organització, conclusions)
Proposta de valor
La proposta de valor és el conjunt de productes i serveis que creen valor per a un segments de mercat específics. L’objectiu és solucionar els problemes dels clients i satisfer les seves necessitats mitjançant propostes de valor. Quin problema ajudem a solucionar? Quin valor oferim als nostres clients? Cal plantejar-ho des de la perspectiva de “què vol comprar el nostre client” versus “què venem”.
Quan es plantegem el model de negoci, identifiquem tres preguntes:
- Com? Activitats relacionades amb la producció.
- Què? La nostra oferta. Aquesta és la que respon la proposta de valor.
- Qui? Activitats relacionades amb la venda.
Per a arribar al Què, podem fer-nos les següents preguntes:
- Què és el que desitjo oferir als clients?
- Quines necessitats dels potencials clients cobriré amb aquest producte o servei?
- Què li proporciono al client que no s’estigui oferint per una altra empresa del mercat?
Donant resposta amb aquestes qüestions podrem establir la nostra proposta de valor. D’aquesta forma podrem establir els criteris de model de negoci, següents:
- Seleccionar els clients potencials als que dirigirem l’oferta
- Crear utilitat per als potencials clients.
- Diferenciar-nos de la competència.
- Aconseguir i conservar als clients
- Com se seleccionaran els clients
Canvas del model de negoci
Per saber com funciona el model de negoci canvas has de saber que és un llenç format per una sèrie d’elements que connecten les diferents parts de l’estructura d’un pla de negoci. És una eina útil i un format cada vegada més sol·licitat. El model canvas per emprendre està compost per 9 fases descrites a continuació:
- Segments de clients: Respon a la pregunta a qui es dirigeix el nostre producte o servei. Descriu el públic objectiu i les seves característiques.
- Proposta de valor: En aquest apartat es tracta d’enfocar els beneficis del teu servei o producte, quina diferència teu pla de negoci al d’altres, quin és el teu punt diferenciador davant la competència.
- Canals de distribució: Vies a través de les quals anem a comunicar la nostra proposta de valor. Els canals que proposa el model de negoci de canvas són: canals propis o externs, directes o indirectes. Aquest segment inclou la descripció de l’efectivitat que generen aquests canals: la notorietat, avaluació, comunicació, distribució i venda.
- Fonts d’ingressos: Com generem els beneficis perquè funcioni el pla de negoci. Aquí s’ha de diferenciar d’ingressos i guanys per no obtenir errors de pressupost.
- Recursos clau: enumera els actius més importants perquè el pla de negoci funcioni. Són els recursos físics, financers, humans o immaterials com les patents o coneixements.
- Relació amb clients: La relació podrà ser personal o automatitzada. Es tracta de tenir en compte en el model de negoci la fidelització i captació de clients i l’estimulació de les vendes.
- Activitats clau: processos claus per al funcionament de l’activitat que es va a exercir. Segons el model canvas les activitats clau d’una negoci són tres: producció, solució de problemes i plataforma.
- Socis clau: aquesta part del pla de negocis amb el model canvas remarca els partners i proveïdors necessaris perquè la idea de negoci funcioni.
- Estructura de costos: segons el model canvas són les despeses en què s’incorre durant el procés de generar valor, és a dir, els costos que genera el negoci. El model de negoci canvas els divideix en: costos fixos i variables, economies d’escala i economies de camp.
El canvas social
- Segments de clients: hauríem de plantejar-nos com generem valor social en el segment de clients i com millorem l’entorn social, per exemple: Que el nostre projecte estigui adreçat a algun col·lectiu en risc d’exclusió social.
- Proposta de valor: hem de plantejar-nos com la nostra proposta fa una producte al servei de les persones, or exemple, podem fer que el nostre producte o servei sigui accessible a persones amb diversitat funcional).
- Canals de distribució: estudiar com el canal de distribució pot ser positiu per a la societat, per exemple si faig lliurament a domicili, la faig amb bicicleta per no contaminar.
- Fonts d’ingressos (situació econòmica a l’inici): és important tenir present que el consum responsable és una màxima per millorar el món en què vivim, ia més pot ser un benefici per al nostre començament, això no vol dir baixar la qualitat, ni el valor que volem aportar, només es tracta d’ajustar bé els números. Per exemple, si volem que el nostre producte sigui ecològic, segurament haurem d’invertir una mica més, però no obstant això el cost social que aportem a la societat també té un gran valor que no és econòmic.
- Recursos clau: serà clau si ajustem molt bé les necessitats, tant des d’un punt de vista d’eficiència, com des d’un punt de vista de consum responsable. També ens ajudarà pensar que si estigueu-vos recursos els trobem al mercat local i de proximitat estem potenciant l’economia dels meus veïns, als que conec i als que tinc confiança. O per exemple en l’aspecte dels recursos humans, el nostre empleats poden formar part d’algun col·lectiu d’inserció, o simplement podem tenir una organització interna democràtica com és el cas de moltes cooperatives.
- Relació amb clients: podem fer que la nostra estratègia de comunicació tingui un impacte social positiu, per exemple si anem a fer la comunicació en paper, podem usar paper reciclat.
- Activitats clau: podem fer que la nostra proposta de valor i per tant la nostra activitat principal tingui un enfocament ecològic o de compromís amb l’entorn local, per exemple que els productes que venc siguin biològics, o en el sector serveis que el meu servei pugui arribar també a un sector de la població que no té recursos econòmics.
- Socis clau (col·laboradors): es planteja que la relació amb l’entorn proper és essencial, potencia l’economia local ens afavorirà i crearà sinergies diferents de consum al nostre voltant. Quan coneixem els canals de distribució, quan els nostres clients coneixen de prop als nostres proveïdors i als nostres col·laboradors, la confiança és un element clau per al consum responsable.
- Estructura de costos (situació econòmica durant el projecte): és important plantejar-se l’enfocament no lucratiu, que ens ve a dir que el projecte cobreix els sous dignes i coherents de les persones treballadores, i que els beneficis es reinverteixen en benefici del projecte o es destinen a alguna obra social o ambiental. També podem plantejar-nos en aquest punt, on guardo els diners mentre no el faig servir i assegurar-me que està en un banca ètica, i no és invertit en caps amb els quals no estic d’acord.
Pla estratègic
Introducció
Molts cops la mateixa dinàmica de la companyia i l’entorn orienta a l’empresa cap a unes estratègies determinades sense necessitats de fer cap pla. Aquestes són les estratègies emergents que, en general, serveix per anar seguint el ritme del sector. En canvi, les estratègies deliberades són les que obtenim del nostre pla estratègic, i són aquestes les que ens porten a canvis importants amb estratègies ofensives.
Algunes consideracions a tenir en compte al definir l’estratègia, són:
- Estan en línia de la identitat de l’organització
- S’enfoquen a mitjà o llarg termini, a partir de 3 anys vista
- Impliquen la posada en marxa una quantitat significativa de recursos
Una bona forma de començar la introducció és definir la situació de l’empresa en el moment d’elaborar el document.
Cal també concretar quin termini cobreix el Pla estratègic. Aquest ha de ser a llarg termini sabent que:
- Pressupostos – s’elaboren a 1 any vista
- Planificació – és la definició de l’estratègia
- Estratègia – és la gestió a llarg termini
Es considera acceptable un Pla Estratègic a més de 3 anys, ja que no pot coincidir amb els pressupostos.
La diferència d’on som i on volem arribar és el Gap estratègic. Per tant cal gestionar el gap per poder evolucionar d’on estem avui. Cal aspirar a un demà ambiciós perquè normalment arribarem un punt més avall.
Visió, missió i valor
La missió, la visió i els valors han d’estar clarament redactats al Pla estratègic, ja que ha de donar sentit al treball diari que realitzi l’empresa. Han de definir un marc prou ampli com per poder ser vàlids al llarg de la vida de la empresa, tot i que poden es poden revisar però no ser subjectes de grans i continuades modificacions. En aquest cas significaria que no estan ben redactats.
Visió
Associada al somni. Què volem ser, on volem arribar.
Respon a les preguntes:
- Què volem aconseguir com a organització?
- Com s’enfrontarà l’empresa al canvi?
- Com es diferenciarà de la resta?
- Com s’aconseguirà ser competitiva?
És possible que en els seus inicis l’empresa passi per dificultats però tingui una visió molt ambiciosa a la que arribar al llarg de la seva trajectòria. La visió dóna a l’empresa una fita per assolir que aporta sentit als esforços que desenvolupen les persones que la integren.
Algunes recomanacions a l’hora de redactar-ho:
- Ha de incloure dos components: una meta ambiciosa a complir en 10 – 30 anys però també una descripció palpable del futur.
- Hem de pensar que ha de ser un punt d’orientació. Ha d’apel·lar tant a la intel·ligència com a les emocions dels treballadors.
Validació:
- Defineix un futur desitjable?
- Motiva?
- Es clara?
- Es concisa?
- Es memorable?
Walmark: ser líder mundial del retail.
Missió
És la raó d’existir de l’organització. Habitualment és útil definir quines línies no volem adoptar per poder obrir ventall al què sí.
Respon a les preguntes:
- Què fem?
- Per a qui ho fem?
- Quines necessitats satisfem?
- Què valoren els nostres clients?
- Com ens diferenciem de la resta?
Walmark: ajudar a estalviar perquè vivim millor (lideratge en costos: economia d’escala, distribució tecnificada, integració tecnològica amb els socis, coneixement dels clients, cultura).
Valors
Els valors ens indiquen quines són les nostres formes particulars per seguir el camí que ens porta a la visió.
Proporcionen un criteri en el moment de prendre decisions incertes, que en alguns casos podrien ser una solució fàcil però que portarien a trair els nostres valors.
Referències
- La cadena de valor de Michael Porter (vídeo 6 min)
- Business Model Canvas Paso a Paso + 2 Ejemplos (vídeo 15 min)
- El canvas social
- Qué es una Startup y cómo funciona éste nuevo modelo de negocio
- Lista Emprendedores: las 50 startups con más futuro
- 17 ejemplos inspiradores de misión, visión y valores de empresas
Vídeos:
- Salvados. La Fageda, cuando negocio y ética van de la mano
- Business Model Canvas Paso a Paso + 2 Ejemplos
- Elevator pitch. Tienes 20 segundos - eduCaixa
- Aprende a hacer la visión, misión y valores en menos de 5 minutos
- Triodos Bank en Buenafuente (La Sexta). Entrevista a Joan Antoni Melé
- El comercio justo en 6 pasos
Estudi de mercat
- Segmentació
- Estudi de mercat
- Referències
Segmentació
1. Segmentació, Beneficis i Característiques de la segmentació
Segmentació de mercat.
Un mercat està format per empreses i consumidors. En els mercats de consum convé segmentar els consumidors en grups de consumidors que tenen les mateixes característiques, és a dir, en grups homogenis.
L’objectiu de la segmentació és aplicar una estratègia comercial diferenciada a cada segment, fet que aportarà més efectivitat a les nostres accions.
La segmentació de mercats permet diferenciar el producte segons les necessitats de cada grup de consumidors. Per exemple: una empresa fabricant de cotxes ofereix: cotxes familiars, cotxes esportius, cotxes compactes urbans, etc. un per cada tipus de consumidor.
Beneficis de la segmentació:
Segmentant coneixem millor els nostres consumidors i disposem de més informació per prendre decisions de màrqueting. Beneficis:
- Permet identificar els segments de mercat més atractius: sigui per creixement de mercat o perquè ens permet identificar segments que estan insatisfets. Després el que caldrà fer és establir prioritats per decidir quins segments satisfer primer.
- Facilita l’anàlisi de la competència. Si la competència ha segmentat i té diferents tipus de productes podem saber què cobreix i identificar millor les seves accions i trobar forats de mercat.
- Permet adaptar el producte a les necessitats del consumidor i satisfer-lo millor: nou producte, nou disseny, reposicionament…
Característiques d’un segment:
Un segment ha de ser:
- Mesurable: Hem d’identificar la mida del mercat (volum de vendes potencials) i el poder de compra dels seus consumidors: renta disponible)
- Accessible: Cal determinar on podem vendre i publicitar el producte.
- Substancial: Cal que tingui una mida mínima per tal que sigui rendible.
- Estable al llarg del temps: Hem de poder rendibilitzar la inversió.
- Cada segment ha de ser diferent dels altres (Ex. pel tipus d’ús del producte, pel comportament de compra…).
2. Criteris de segmentació
Criteris de segmentació
Quan segmentem un mercat podem dividir-lo segons uns criteris:
- Generals: Que no tenen relació amb el producte.
- Específics: Que tenen relació amb el producte.
- Objectius: fàcils de quantificar.
- Subjectius: No tan fàcil de quantificar.
Criteris objectius
- Generals
- Demografia: Unitats familiars, població urbana/rural, edat, sexe.
- Socioeconòmics: Nivell econòmic.
- Específics
- Ús del producte:
- Continu/1 sol cop. Ex. lentilles, càmeres.
- Familiar /individual (menjar precuinat).
- Ús freqüent o esporàdic. Ex. crema solar o crema diària per la pell.
- Lloc de consum o de compra: cosmètics de viatge o de casa, ex. consumidors de llibres en màquines del metro o consumidors de llibres en llibreries, Cola-Cao en sobres per als bars i Cola-Cao en pot per a les famílies).
- Fidelitat a la marca: programa de punts frequent flyer per aquells clients que són fidels.
- Categoria d’usuaris: nou client /antic/ regular… ex. promoció per a nous clients d’un banc o de telefònica o per a subscriptors d’un diari.
- Ús del producte:
Criteris subjectius
- Generals
- Personalitat: Extravertit / introvertit, prudent /arriscat, rata /generós, confiat / desconfiat, perfeccionista / indiferent…
- Estil de vida: regularitats que s’observen en la conducta de les persones en diferents situacions canviants de la seva vida. Forma de viure. Com ho calculem: Segons les opinions de la gent, els interessos (salut / oci) o la cultura (família, sexualitat, treball…) Veiem com gasten els diners i el temps en activitats, treball, compres… Ex. estils de vida: JASP.
- Específics
- Actituds, percepcions, preferències dels consumidors: Ex. grau de risc que accepten: cotxes segurs… Percepcions sobre l’obesitat (per això fan productes light), percepcions sobre el medi ambient (productes reciclables).
- Beneficis o avantatges que busquem en el producte… el motiu de la compra. Ex. diferents segments en funció del que esperen per la compra d’una minicadena hi-fi… un de més tècnic es preocupa pel so, un altre voldrà un disseny especial, un altre valorarà que sigui econòmic…
3. Estratègies de segmentació
Hem vist que podem segmentar el mercat per criteris generals (que no tenen a veure amb el producte, com p. ex. la demografia) i específics (relacionats amb el producte: com el seu ús: ampolla familiar o d’ús individual) i a la vegada per criteris subjectius (difícils de mesurar, com per exemple la personalitat) i objectius (com per exemple la categoria d’usuari).
Estratègia indiferenciada
Consisteix a aplicar la mateixa estratègia comercial a tots els segments que hem determinat. Busca cobrir les necessitats comunes més que trobar les diferències.
Considera que les diferències existents entre cadascun dels segments no són suficientment importants com per fer estratègies diferents.
La distribució i la publicitat són generalment massives.
Els costos de l’estratègia són menors que d’altres, ja que estalviem en termes de producció (productes únics, menys colors, formes…), distribució (menys temps negociant amb diferents canals) i comunicació (no cal adaptar-la a cada segment).
L’inconvenient d’aquesta estratègia és que no és optima en els mercats on hi ha molta competència o quan existeixen segments molt heterogenis. I avui en dia la majoria de mercats ja estan molt segmentats i costa trobar un producte que s’adapti a grups heterogenis. És difícil competir amb altres empreses que sí que fan la diferenciació i satisfan millor als clients.
Per exemple, en un primer moment Coca-cola només oferia una versió del seu producte, esperant que fos del gust de tothom, més endavant va anar adaptant el producte a diferents segments: light, sense cafeïna, sense cafeïna i light…
A més a més, l’aparició de nous mitjans de comunicació i canals de distribució ha ocasionat que aparegui la possibilitat de segmentar més el mercat i que sigui més difícil aplicar una estratègia comercial única. No farem igual una campanya per internet que en un gran hipermercat o en una botiga detallista.
Estratègia de segment diferenciada
L’empresa analitza el mercat i estableix diferents segments als quals aplicarà una estratègia concreta i diferenciada una de l’altra per cobrir millor les necessitats dels clients.
Aquest és el cas de les agències de viatges que tenen en compte el Cicle de vida de les famílies (el cicle de vida familiar significa que hi ha diferents etapes en la vida familiar normal):
- Etapa de solters: persones joves sense vincles matrimonials.
- Parelles casades joves sense fills.
- Niu ple. Parelles casades joves amb fills.
- Nius plens. Parelles casades, de major edat, amb fills encara dependents.
- Niu buit: parelles casades de major edat sense fills dependents.
- Persones de major edat que viuen soles. Encara treballant o ja jubilades.
Podem adaptar una estratègia comercial diferent per a cadascun d’aquests segments.
Avantatges: l’empresa esdevé més eficient perquè es concentra esforços en satisfer al client adaptant el producte, escollint el canal i realitzant una comunicació específica per al client. Quan el mercat està molt segmentat no trobem tants competidors perquè adaptar-se als segments implica un cost elevat, guanyem doncs, quota de mercat i fidelitat envers la marca. Si som forts en cada un dels segments, podem aconseguir ser més eficients que dirigint-nos a la totalitat del mercat amb un sol producte, ex. Procter&Gamble amb el sabó de la roba.
Inconvenients: Cost ja que gastem més en adaptar la producció, en controlar els diferents canals i en imaginar diferents estratègies de comunicació. Una excessiva segmentació ens pot portar a confondre al consumidor, canibalisme entre productes i una disminució de la rendibilitat (no hi ha economies d’escala).
Estratègia de segmentació concentrada: nínxols
L’empresa decideix atendre a un segment o a un subsegment del mercat però no a tots perquè no té la capacitat interna suficient o perquè les condicions del mercat no són favorables. Un subsegment pot ser per exemple, dins de la categoria de cotxes utilitaris, el subsegment pick-up i el subsegment utilitari esportiu.
Els segments són generalment grans i els subsegments o nínxols són més petits i normalment atrauen menys competidors.
Pex. Bentley es dirigeix a un subsegment: cotxe de luxe, alta qualitat, bon servei i status.
L’empresa es dirigeix al segment on té un avantatge competitiu que d’altres no tenen on que no volen utilitzar perquè no tenen prou experiència o no consideren que sigui un mercat en creixement.
La producció, distribució i comunicació són molt específiques per al segment.
Avantatge: Aconsegueix una bona quota de mercat perquè s’especialitza.
Inconvenient: Estratègia molt sensible als canvis de les preferències del consumidor i a l’aparició de nous competidors.
4. Màrqueting mix, Distribució o Comunicació
A partir de la concreció de les variables del màrqueting mix (producte, preu, distribució i comunicació) podem segmentar segons els criteris corresponents.
Per producte
Podem oferir diferents models d’un mateix producte diferenciats per:
- Hàbit d’ús: envàs familiar o individual: aigua
- Lloc d’ús: a casa, de viatge: raspall de dents
- Marca: creem una segona marca com per exemple Sony i Aiwa.
- Complexitat del producte: mòbils normals o avançats.
Per preu
Podem oferir un preu diferent a cada segment, sigui per una oferta temporal o per una categoria d’usuaris especial: ex. Els subscriptors tenen un 20% de descompte en entrades al teatre o, si contractes un pac de telefonia abans d’una data X, tens una bonificació. Podem fer una compra en tres mesos de temps i tenim un dte.
Dia de l’espectador, dias azules de RENFE…
Per distribució
Escollim acuradament el canal de distribució que volem fer servir per fer arribar el nostre producte. No trobarem certs perfums a perfumeries de barri o certes marques de roba a qualsevol botiga.
Canal exclusiu: Només fem arribar el producte per un sol canal de distribució ben seleccionat.
Canal selectiu: Escollim els canals que compleixin amb certes característiques (per exemple Mango només selecciona locals que estiguin al centre de la ciutat en les avingudes més importants).
Distribució intensiva: maximitzar la presencia en tota mena de canals: ex. cacauets que trobem tant a supermercats, màquines de vending, quioscs, bars…
Per comunicació
Escollirem el mitjà de comunicació que millor satisfaci les nostres necessitats de comunicació i a la vegada, detallarem les característiques, p.ex. televisió, a una franja horària X, premsa de menors de 25 anys, a la pàgina del mig…
Certes marques de roba fan publicitat esponsoritzant campionats de golf o de polo, concentrant-se en sectors de la població minoritaris però amb alt poder adquisitiu.
Estudi de mercat
L’anàlisi de l’entorn i l’estudi de mercat són aspectes molt importants en tot projecte empresarial. Es tracta de detallar el mercat en el qual l’empresa mantindrà l’activitat principal, així com els clients potencials i la competència. Una empresa ven productes i serveis i, per tant, necessita clients disposats a comprar-los. És per això que l’estudi de mercat ens ha de permetre analitzar qui són aquests clients; les necessitats, els desitjos, les demandes i les expectatives que poden tenir; com es comporten a l’hora de comprar i de quina manera haurem de respondre a tot això.
1. Característiques del sector
Qualsevol emprenedor ha de conèixer el sector on es desenvoluparà l’activitat, és a dir, ha de conèixer els clients, els proveïdors, la competència, l’amenaça de nous competidors, les possibles aliances (col·laboradors), les barreres d’entrada existents i els productes o serveis substitutius. També és habitual analitzar la concentració o dispersió de les empreses, l’evolució i les perspectives futures, el volum de facturació, les regulacions del sector i els permisos necessaris per actuar-hi, entre altres.
2. Anàlisi del mercat
Es tracta d’analitzar el mercat en què l’empresa desenvoluparà l’activitat i identificar les forces competitives que el configuren.
3. Àmbit, evolució i tendències
- Zones geogràfiques on es preveu comercialitzar el producte o servei (barri, municipi, comarca, entre altres). Cal diferenciar entre el mercat real, aquell que actualment compra o consumeix el producte o rep el servei, i el mercat potencial, aquell que pot comprar o consumir el producte o servei de l’empresa independentment que ja ho faci o no.
- Tendència i evolució del mercat: s’ha de conèixer si aquest mercat pateix una evolució a l’alça o a la baixa, i en quina proporció respecte a anys anteriors, o bé si efectua una desviació cap a productes o serveis semblants.
- Volum del mercat: calculat en unitats, en euros, en quilos, amb la màxima segmentació possible (en àrees geogràfiques, per canals de distribució i d’altres).
- Possibles canvis en la demanda.
- Quota de participació estimada de l’empresa: part del mercat que compra o consumeix el producte o servei de l’empresa en relació amb el total de compradors o consumidors del producte genèric.
4. Segmentació del mercat
Segmentar el mercat és agrupar els clients en grups similars en funció de les seves necessitats i dels seus hàbits, que solen estar vinculats a criteris demogràfics, geogràfics, socioeconòmics, i altres. Amb aquesta segmentació podrem establir plans específics per a cadascun d’aquests segments homogenis i pensar en les raons per les quals el producte pot satisfer-ne les necessitats.
5. Anàlisi dels clients
Es tracta d’aprofundir en el coneixement dels clients i arribar a comprendre’n el comportament. Caldrà, doncs, determinar quins seran els clients potencials de l’empresa. Aquests clients poden ser particulars (consumidors finals), dels quals hauríem de definir-ne el perfil (sexe, edat, estat civil, poder adquisitiu, nivell cultural, localització geogràfica, hàbits de consum, entre altres), però també poden ser empreses, administracions públiques o associacions, fundacions, i d’altres.
En qualsevol cas, siguin del grup que siguin, és important determinar qui són, on són, què necessiten i què demanen, i quines millores desitjarien respecte als productes que ara ofereix la competència i en què basen les seves decisions de compra.
Respondrem les següents preguntes sobre els clients:
- Qui compra? Característiques personals.
- Per què compra? Motivacions.
- Què compra? Productes i marques.
- Com compra? Busca el producte o compra el que se li ofereix.
- Quant compra? Quantitats.
- On compra? Establiments, context, distància.
6. Anàlisi de la competència
En aquest apartat s’ha d’analitzar la competència més directa, és a dir, les empreses que ofereixen els mateixos (o similars) productes o serveis i que s’adrecen al mateix públic. És important no limitar-se a fer una llista d’aquests competidors, ja que cal conèixer els aspectes més importants que els caracteritzen:
- Identificar quins competidors hi ha.
- On són, en quines zones operen i quina és la seva quota de mercat.
- A qui venen i quina és la imatge que té d’ells el client potencial? Tenen prestigi?
- Quins productes o serveis ofereixen i amb quines garanties? Són innovadors? Tenen qualitat?
- Quina és la seva política de preus, descomptes i condicions de pagament?
- Inverteixen part del seu pressupost en promoció i publicitat?
- Quina estratègia competitiva utilitzen? Quins avantatges tenen, quines són les seves mancances, i per què tenen èxit o per què no?
7. Anàlisi dels intermediaris
En alguns sectors, si l’empresa no ven directament al client, és important conèixer els intermediaris (distribuïdors, detallistes, entre altres), perquè incideixen en la qualitat i la imatge que es dóna. Cal saber qui i quants són, com treballen i com poden agregar valor a l’empresa.
8. Anàlisi dels proveïdors
Els proveïdors influeixen de manera directa en la qualitat dels productes o serveis d’una empresa. S’ha de conèixer els possibles proveïdors i identificar els que ofereixin avantatges competitius als productes o serveis que ens disposem a desenvolupar. En general, cal escollir els proveïdors que ens ofereixin una qualitat acceptable a un preu raonable, tenint en compte també els terminis de pagament i els descomptes o ràpels, però sense oblidar els terminis de lliurament, ja que poden ser crítics en alguns processos productius.
Referències
Màrqueting
- Màrqueting digital
- Tipus de màrqueting
- El màrqueting inbound
- SEO
- Calendari editorial
- Mesurament
- Estratègia outbound
- Eines
- Referències
Màrqueting digital
El màrqueting digital (o en línia) consisteix a contactar possibles clients aprofitant l’internet i tots els seus canals, entre d’altres:
-
Cercadors
-
Xarxes socials
-
E-mail
-
Portals web
A més d’utilitzar-los, podem obtenir feedback (automatitzat) per a modelar l’estratègia que hem dissenyat per a aconseguir l’objectiu que hem definit.
Procés
El procés de l’acció de màrqueting digital podria ser el següent:
Objectius ⇨ Estratègia ⇨ Mesurament ⇨ Optimització
-
Establir objectius que volem aconseguir. Caldrà definir els compradors i analitzar la competència.
-
Establir la nostra estratègia, definint els indicadors que determinaran si els objectius s’han complert**.**
-
Executar l’estratègia i mesurar les taxes de conversió (accions previstes de la nostra estratègia que s’han complert) utilitzant les eines adients.
-
Optimitzar els resultats, especialment si els resultats no han estat bons, replantejant la nostra estratègia.
Objectius
El màrqueting digital requereix un pla amb objectius ben definits. Aquests objectius han de tenir unes característiques, anomenades SMART:
-
específic: els objectius estan clarament definits i exposats, de manera que tot l’equip entén l’objectiu i per què és important
-
mesurable (analítica web)
-
assolible,
-
orientat a resultats
-
basat en un temps delimitat
Alguns possibles objectius de màrqueting: promocionar nous productes o serveis, créixer en presència digital, generar leads, dirigir-se a nous clients, retenir clients existents, construir coneixement de marca, desenvolupar lleialtat de marca, incrementar vendes i/o beneficis, expandir-se a un nou mercat, fer créixer el teu share, convertir-se en un referent d’autoritat al sector.
Tipus de màrqueting
Podem fer una primera classificació d’estratègies de màrqueting:
-
La outbound, o màrqueting tradicional, on l’objectiu és vendre en un sol sentit, sense comunicació des dels usuaris.
-
La inbound, basada en màrqueting de continguts, on l’objectiu és que l’usuari et trobi a tu en lloc d’anar a buscar-lo, en crear un canal. Això s’aconsegueix amb creació de continguts i el mesurament d’objectius.
L’estratègia outbound és la tradicional, però també té una part digital: el pagament als cercadors i anuncis als mitjans digitals. Aquesta estratègia es considera avui en dia superada si s’utilitza de forma aïllada, tot i que pot ser un complement a la inbound.
El màrqueting inbound
Ens cal arribar al nou consumidor. L’estratègia consisteix a formar una relació de valor en el temps que condueixi de forma natural a la compra, satisfent els seus desitjos i resolent els seus problemes.
El cicle de vida del màrqueting inbound seria:
-
Atreure els desconeguts perquè siguin visitants.
-
Convertir els visitants en oportunitats de venda.
-
Tancar les oportunitats i fer clients.
-
Delectar als clients i convertir-los en promotors.
Una forma més general de veure el cicle de vida de les compres és el funnel.
Funnel
El funnel o embut de conversió defineix els diferents passos que ha de donar un usuari o visitant per a convertir-se en client.
Una conversió es produeix quan el nostre client potencial executa una acció clau que hem definit en la nostra estratègia de màrqueting. Això pot convertir un visitant en un lead, o en general, fer que el client potencial progressi dins del funnel.
Les fases del funnel són:
-
TOFU (top of the funnel): és l’etapa de descobriment de la marca. L’usuari busca contingut educacional que l’ajudi a identificar, definir i comprendre el seu problema i estableix els requeriments de la solució.
-
MOFU (middle): l’usuari ja coneix el seu problema i entra en una fase de consideració en la qual busca possibles solucions. Detecta les empreses que poden oferir-li un producte / servei d’acord amb les seves necessitats i requeriments.
-
BOFU (bottom): l’usuari analitza les possibles opcions i selecciona l’empresa que millor solucioni el seu problema. És a dir, pren una decisió.
Cada fase té diferents recursos per fer progressar els nostres clients potencials.
-
TOFU: Consciència. Hem de detectar aquests problemes o necessitats, encara que no tinguem la solució. En aquesta etapa els formats més utilitzats són guies, eBooks, blog post o white papers. Tècniques: SEO, anuncis, xarxes, vídeos, linkbuilding, tràfic directe (orgànic). Volem generar leads.
-
MOFU: Consideració. Hem de proporcionar contingut valuós que ajudi en aquesta decisió. Vídeos, podcast, comparacions de productes, webinars o guies d’expert són els formats més efectius en aquesta fase. Tècniques: drip màrqueting, missatges dirigits al llarg del temps, en format correu o altres.
-
BOFU: Conversió. Hem d’oferir una solució que porti al client potencial a una futura venda. Els millors formats per a aquesta fase són casos d’estudi, demo de producte o documentació del producte o servei. Tècniques: landing pages.
Buyer persona
Per poder definir la nostra estratègia, s’utilitza el buyer persona: una representació semi-ficticia del client ideal, amb dades demogràfiques, patrons de comportament, motivacions, objectius i reptes. Aquesta construcció es basa en dades, no en suposicions.
Com podem fer-ho?
-
Recollint dades demogràfiques en enquestes en línia.
-
Analitzant el tràfic dels teus portals web, que ens ofereix informació demogràfica, paraules clau, etc.
-
Utilitzant informes i estudis oficials i d’empreses especialitzades.
Aquesta informació ajudarà a establir patrons i tendències que definiran l’estratègia de màrqueting i vendes.
Leads
Un cop tenim la nostra buyer persona, podem establir estratègies per a conduir als clients potencials cap a la compra. L’estratègia inbound requereix aconseguir leads. Un lead és un usuari que ha lliurat les seves dades a una empresa i que, com a conseqüència, passa a ser un registre de la base de dades amb el qual l’organització pot interactuar.
Es poden aconseguir leads amb diferents mètodes promocionals: un portal informatiu, landing pages (regals), un blog, xerrades, esdeveniments i fins i tot anuncis (màrqueting tradicional outbound).
Un cop tenim leads, els hem d’organitzar en una base de dades que ens permeti fer un seguiment dels clients potencials. Els leads poden acostar-se fins a ser clients. Això es pot veure reflectit al funnel.
Cercadors i paraules clau
Els cercadors han de ser amics de la nostra estratègia. Hem de trobar les paraules clau (o keywords) per al servei o producte que oferim, ja que aquestes seran les que utilitzin els nostres potencials clients.
L’objectiu és que el tràfic arribi al teu portal web de forma orgànica, és a dir, natural, mitjançant el posicionament dins dels motors de cerques (Google, principalment).
En aquest enllaç, Google explica com funciona el seu cercador. Aquesta pàgina explica que, a l’hora de fer la cerca, es tenen en compte coses com:
-
paraules de la consulta
-
La rellevància i usabilitat de les pàgines
-
El nivell de coneixements de les fonts
-
la teva ubicació i configuració
Les paraules clau han d’incloure aspectes com:
-
El nostre producte o servei
-
El nostre aventatge competitiu
-
El nostre públic objectiu
Segons la intenció de l’usuari, els keywords poden ser:
-
Informatius: l’usuari busca informació.
-
Transaccionals: l’usuari té intenció de convertir.
-
Navegacionals: l´usuari vol anar a cert lloc.
Segons el volum de cerques, els keywords poden anar dels més genèrics als més concrets:
-
Head (genèriques), amb molta competència i poca conversió.
-
Middle Tail: al mig
-
Long Tail: cerques més específiques, amb poca competència i més conversió. Les més interessants per començar.
Per fer un estudi senzill, podem començar per Google Autosuggest, Google Trends o Google Ads (Keyword Planner).
Estratègies i tècniques
-
SEO: es vol millorar l’autoritat i rellevància d’un portal mitjançant bon contingut amb paraules clau, HTML ben estructurat, contingut ben estructurat, bon temps de càrrega, bona experiència UX, bones URLs. També factors externs: qui ens enllaça i amb quina autoritat.
-
SEM: anuncis, utilitzant keywords (paraules clau) (Google Ads), Facebook Ads, vídeos, etc.
-
Màrqueting de continguts (orgànic): creació i distribució de contingut rellevant per a atreure un públic objectiu definit. Pot ser web, vídeo o xarxes socials. Per exemple: infografia, fotografia, posts de cites, vídeos, gràfics de dades, captures de pantalla, instruccions pas a pas, call to action (CTA), preguntes / questionaris, memes, gifs animats, e-books.
-
Landing pages: conversió de visitants a leads.
-
Remàrqueting: detecció de visitants que tornen (Google Ads).
-
Disseny responsive (accés multiplataforma als nostres continguts).
-
Email màrqueting.
-
Cerques locals: Google My Business, gestió d’opinions.
-
Lead scoring: es tracta de valorar numèricament la proximitat de l’usuari al client ideal. Això ajuda al procés de personalització de la comunicació.
-
Automatització de màrqueting: permet generar fluxes de treball per gestionar leads (Hotspot). També relacionat amb el lead nurturing: automatització de les interaccions amb l’usuari.
-
Màrqueting d’influencer.
-
Tests A/B.
SEO
Tenim dos tipus de SEO, o sigui, aspectes que poden afavorir la visibilitat orgànica del nostre portal web dins dels cercadors:
-
Off-page SEO: coses que no depenen directament del nostre control, especialment, backlinks que milloren el CTR (Click-Through Rate).
-
On-page SEO: inclou optimització de paraules clau, temps de càrrega, experiència d’usuari, optimització del codi i format de les URL.
També tenim una altra classificació, en funció de la valoració ètica de les tècniques que utilitzem:
-
Black Hat SEO: tècniques poc ètiques, o que contradiuen les directrius dels cercadors. És una estratègia de curt termini, arriscada i que no aporta valor. I més important: si s’utilitza, els cercadors poden penalitzar el portal, i fer-lo perdre rellevància.
-
White Hat SEO: tècniques ètiques, alineades amb les directrius dels cercadors.
Contingut
El contingut és el més rellevant per aconseguir posicionar-se, molt més que les paraules clau, les etiquetes, o els backlinks. I molt més que l’aspecte.
La qualitat del contingut depèn de diferents factors, pot ser: utilitat, educatiu, entretingut, rellevant, informatiu, original i autoritatiu (respectat). No ha de ser: promocional, publicitari, per a l’empresa, spam, fet només de paraules clau, copiat.
La qualitat determina el teu rànquing, ja que determina quines paraules clau són rellevants, i si atreu o no tràfic d’altres portals, incrementant la teva autoritat al cercador. Alguns consells:
-
El contingut s’ha de dirigir al teu client, i ho ha de fer molt llegible i entretingut. Però també als cercadors: millor utilitza paraules que imatges, vídeos o animacions.
-
No repeteixis les paraules clau de forma no natural, els cercadors ho detecten.
-
Els primers paràgrafs són molt importants. Escriu com si fos un diari: primer el més important.
-
No escriguis texts massa llargs, però tampoc massa curts, es diu que 1000 paraules per pàgina és una bona mida.
Paraules clau
Són paraules que s’inclouen en una cerca d’un usuari. Per tant, si volem que els cercadors ens trobin, han d’aparèixer al nostre portal. Hem d’investigar quines són les millors per al nostre benefici, i això es fa amb eines (habitualment de pagament), mai de forma intuïtiva.
Un cop les tenim, hem de decidir com les utilitzem:
-
Al text de ancoratge (text visible de l’enllaç).
-
En certes etiquetes HTML:
<title>, <h1>, <h2>, <h3>, <meta name="keywords">(menys important avui en dia). -
En l’atribut ALT de les imatges
<img>. -
En el text del portal web, de format natural, utilitzant la densitat correcta.
-
Utilitzant-les a diferents seccions del portal, en funció de quina secció hi som, i amb cua més o menys llarga (head / tail).
-
Envolta-les d’etiquetes d’èmfasi:
<em>, <strong>, <b>, les aranyes ho detecten.
Etiquetes HTML
Són menys importants que el contingut (text), però poden ser significants:
-
Títol
<title>: es veu a la pestanya navegador, però especialment important és que apareix a la capçalera d’una entrada de la SERP.-
El títol no hauria de ser de més de 64 caràcters, i entre 3 i 10 paraules.
-
Ha d’incloure el nom de la pàgina i paraules clau. Un format possible és “Títol: algunes paraules clau”.
-
-
Capçaleres
<h1>...<h6>: les aranyes les llegeixen per entendre com s’organitza jerarquicament el portal. -
A les imatges
<img>, utilitza l’atribut ALT i TITLE per descriure la imatge. -
Si pot ser, utilitzar sempre etiquetes amb contingut semàntic (capçaleres), que no aquelles que només serveixen per organitzar visualment (p. ex.
<div>). -
<meta name="description">inclou el resum de la teva pàgina. -
<meta name="keywords">inclou les paraules clau, tot i que els cercadors ja no solen utilitzar-ho.
Disseny i organització
Segueix alguns principis senzills:
-
La jerarquia no hauria de tenir massa nivells, facilita la feina als visitants i als cercadors.
-
La informació s’ha d’estructurar de head a tail, amb la informació més important a les pàgines més dalt de la jerarquia.
-
És important treballar amb les paraules clau per pàgines.
-
Les pàgines importants s’han de poder arribar des de la navegació de la pàgina principal.
-
No duplicar contingut: millor, tenir diferents camins per arribar al mateix contingut.
-
Les URL han de ser no massa llargues, i incloure paraules clau.
-
Testeja la navegabilitat interna de l’estructura.
Enllaços d’entrada
La quantitat i especialment la qualitat (millor rànquing) dels enllaços d’entrada (backlinks) afecta el teu rànquing. A més, si una pàgina té pocs enllaços de sortida, i està relacionat amb els continguts del teu portal, els seus enllaços compten més.
Alguns consells (White Hat):
-
El més important: crea contingut que altres portals vulguin enllaçar.
-
Si vols que t’enllacin, fes córrer la paraula: emails, xarxes socials, notes de premsa.
-
Fes peticions personals a portals de qualitat (no automatitzades) perquè t’enllacin (webmasters, bloggers, altres contactes).
-
Negocia amb altres portals de qualitat l’intercanvi d’enllaços, però evita que sigui automatitzat.
-
Utilitza related:domain per veure on es parla d’un domini.
Comprar enllaços és considerat Black Hat.
Altres tècniques
Altres tècniques a considerar:
-
Afegir el teu portal als cercadors perquè sigui indexat. A Google, mitjançant la Search Console.
-
Crear sitemaps per ajudar als cercadors a entendre l’estructura del teu portal. Veure sitemaps.org.
-
Utilitzar rich snippets (fragments enriquits) són segments de codi HTML amb contingut que expressa la seva funció dins de la web. Pots mirar-te la pàgina schema.org.
-
Integrar xarxes socials:
-
T’ajuda a dirigir tràfic cap a la teva web.
-
Algunes xarxes socials també s’indexen. Per exemple, twitter, linkedin, pinterest, facebook.
-
Proporcionen backlinks, especialment importants si es tracta d’influencers.
-
Si vols rebre atenció de les xarxes, el teu contingut ha de ser fresc.
-
Participa en les xarxes socials en què la teva activitat pugui beneficiar-se.
-
Afegeix botons de compartir a les teves pàgines.
-
Calendari editorial
Un calendari editorial és una previsió temporal de publicacions que és la forma de gestionar i controlar les publicacions al llarg de diversos mitjans per promocionar la teva marca.
La primera cosa que cal és decidir la periodicitat de cada tipus de publicació que es pot fer. Un cop decidida, el calendari es pot gestionar amb un senzill full de càlcul, que tingui per una banda les dates del calendari i per una altra els camps de cada publicació.
Per cada publicació, aquests són els camps recomanats:
-
Creador: qui escriu. Pots tenir també convidats externs.
-
Categoria: mitjà o temàtica de la publicació.
-
Estat: pots definir els teus propis. Una proposta seria: ajornat, estudi de paraules clau, començat, preparat, publicat.
-
Objectiu: quin és el propòsit de la publicació, dins del teu pla de màrqueting?
-
Títol: el títol que tindrà un cop publicat.
-
Paraules clau: quines paraules clau vols utilitzar.
-
Altres: inclou qualsevol altra informació útil per a tu o el creador.
Mesurament
El mesurament del resultat de les nostres estratègies quantificables se sol fer amb KPI (Key Performance Indicators):
-
Et permeten mesurar el rendiment d’un procés.
-
Representen un valor relacionat amb un objectiu que s’ha fixat anteriorment (recorda: SMART).
-
Normalment, s’expressen com un percentatge de consecució d’aquest objectiu.
A continuació veurem alguns exemples de KPIs en funció de certs objectius.
-
Augment de vendes: unitats o ingressos associats, especialment al llarg de campanyes o iniciatives.
-
Millora de beneficis: marges aconseguits després de treure despeses.
-
Share del mercat: hauràs de comprovar els ingressos que generes en relació als que genera el teu mercat.
-
Generació de leads: nombre, increment, cost dels leads i taxa de conversió de les visites a la web.
-
Obtenció de nous clients: especialment al començament, comptar el nombre, l’increment, el cost per client (quan ens ha costat obtenir-lo) i el percentatge de leads que es converteixen en clients.
-
Lifetime value d’un client: quan es vol mantenir el negoci, compta els clients que retornen (nombre i percentatge) i quan gasten al llarg de la seva vida.
-
La despesa individual: quan es gasta en cada compra, per cada perfil (si es té).
-
Les taxes de conversió: quan hi ha campanyes (landing pages, email links, proves gratuites, etc.), mesura la conversió, o sigui, quin percentatge que gent fa l’acció desitjada quan se li presenta.
-
Mètriques web: nombre de visites, visitants únics, pàgines per visita, taxa de rebot (abandonament després de visitar una pàgina), temps mitjà a la web.
-
Involucració a les xarxes social: nombre i increment de seguidors, nombre de comentaris, comparticions, generació de leads, opt-ins, tràfic cap a la web.
-
Rendiment SEO: rellevància de la web (segons diferents eines SEO), backlinks.
Estratègia outbound
Una estratègia outbound pot ser un complement de la teva inbound. Especialment al començament del teu negoci, ja que l’estratègia inbound és més de llarg termini.
A més, pot ser una estratègia personal per ajudar a convertir visitants que han arribat via inbound.
Aquestes són algunes claus:
-
Trucades i emails en fred. És important personalitzar la comunicació, especialment a les trucades. Cal fer primer una recerca, intentant identificar possibles objectius, potser amb eines de generació de leads.
-
Correu convencional. Pots obtenir leads de públic offline. Es pot automatitzar amb eines, i fer seguiment online. Es pot combinar amb el món digital, com estratègia complementària.
-
Anuncis a les xarxes i als cercadors. És complementari de l’estratègia de continguts. És important dirigir bé els anuncis cap a segments concrets. Especialment, si podem fer-ho per a nous lectors fora del canal inbound.
-
Fires, conferències i networking. Permeten obtenir nous leads cara a cara. Podem anar a esdeveniments existents o organitzar els nostres.
Eines
Hotspot és una de les eines de referència, però en tenim moltes:
-
Un sistema de gestió de continguts (CMS), com ara Wordpress o Squarespace.
-
Una eina de màrqueting per correu electrònic / generació de leads, com Mailchimp, Convertkit o Aweber.
-
Un creador de land pages, com ara Leadpages. Això és opcional perquè les pàgines de destinació es poden fer amb Wordpress i Squarespace.
-
Una eina d’automatització de màrqueting, com ara Autopilot. Això és opcional perquè l’automatització de màrqueting sovint ja està integrada en les eines de màrqueting per correu electrònic o en les eines de generació de leads.
-
Un calendari editorial: és el document que ens dirà què hem de publicar, quan ho hem de fer, on el publicarem i qui serà la persona que l’ha de fer.
Referències
- ¿Cómo crear un plan de Marketing Online paso a paso? (vídeo 19 min)
- Cómo hacer un plan de márqueting digital paso a paso
- Como hacer un calendario editorial
- 40 de fiebre
- InboundCycle
- Google Trends
- How me make money (Google)
- ¿Qué es el Inbound Marketing? Metodología y caso práctico
- Inbound Marketing: ¿Qué es y por qué usarlo hoy?
- Cómo hacer una auditoría de Marketing Digital
- Marketing Automation en Hubspot
- Tutorial de búsqueda de keywords
- The Content Marketer’s Guide to Keyword Research
- The Ultimate Sales Funnel Guide for Small Business
- 16 Marketing KPIs You Need to Monitor in 2024
- What is Google Ads
- Guía SEO para principiantes
- ¿Quieres comprar un perro?
- Here are the outbound marketing tactics that still work in 2019
- Energía en edificios de oficinas
Estructura legal i pla econòmic
- Estructura Legal
- Pla Econòmic i Financer
- Referències
Estructura Legal
1. Definició Estructura Legal
L’estructura legal d’una empresa es conforma per diferents aspectes com són la forma jurídica que aquesta adoptarà (individual, societària o col·lectiva), els tràmits que es duran a terme per constituir-la i posar-la en marxa, i finalment les obligacions fiscals que tindrà arran de la forma jurídica escollida.
És convenient que pensis quins són els avantatges i desavantatges de les diferents formes jurídiques i quina implicació tindran en el teu model de negoci perquè la presa de decisió sigui la més adient per la teva empresa.
2. Forma jurídica
Abans d’iniciar qualsevol tràmit d’inici d’activitat, cal estudiar atentament la fórmula més convenient per crear l’empresa, a fi i efecte de determinar quina estructura s’adapta millor a les característiques pròpies del projecte que es vol desenvolupar.
Les diferents formes jurídiques són els tipus d’empresa que l’administració preveu que poden constituir-se segons el nombre de socis, el tipus de responsabilitat i el capital social aportat. De fet, la forma jurídica escollida per a l’activitat econòmica de l’empresa en determinarà en gran manera el sistema d’organització.
A l’hora de prendre una decisió és important conèixer el ventall de formes jurídiques que la llei recull, els seus requisits, els avantatges i els inconvenients de cada tipologia. Entre els aspectes que cal valorar abans d’optar per una forma jurídica podem assenyalar:
- La complexitat de la constitució i la gestió. Certes formes requereixen més burocràcia, o certes activitats requereixen certa forma. També els costos poden variar.
- El nombre de socis. Podem tenir formes amb una sola persona o amb diverses.
- Les necessitats econòmiques del projecte. Les formes poden requerir capital mínim.
- Els aspectes fiscals. Bàsicament, tenim dos grups: les formes subjectes a l’Impost sobre la Renda de les Persones Físiques (IRPF) i les sotmeses a l’impost de societats (IS).
- La responsabilitat patrimonial dels promotors. Variable segons la forma. Pot ser:
- Subsidiària: una persona assumeix la responsabilitat.
- Solidària: es pot exigir el deute a qualsevol soci.
- Mancomunada: cada soci respon segons l’aportació al capital social.
- Llibertat d’acció de l’emprenedor. Quan hi ha formes amb diversos socis, és possible que la decisió sigui en funció del capital aportat, o bé pot ser un vot per persona.
- Imatge. Les formes unipersonals donen una imatge menys sòlida.
- Accés a ajuts públics. Algunes formes tenen ajuts, tot i que no hauria de ser un criteri important.
2.1 Formes jurídiques individuals
Les formes jurídiques individuals realitzen la seva activitat amb el nom de l’empresari.
- Empresari individual: autònom.
- Comunitat de béns.
- Societat civil particular.
Les formes individuals estan gravades amb l’IRPF, un impost progressiu.
Les rendes gravades amb l’IRPF són:
- Rendiments del treball (sou).
- Rendiments del capital mobiliari (interessos de comptes, dividends).
- Rendiments per activitats econòmiques (empresaris). Per fer el càlcul de beneficis, tenim tres possibles règims:
- Estimació directa normal: per a grans empresaris (facturació > 600K).
- Estimació directa simplificada: petits empresaris (facturació < 600K).
- Estimació objectiva: per a activitats concretes, rendiments < 150K. Càlcul fix per cada mòdul segons treballadors i mida del local.
- Guanys i pèrdues patrimonials (transmissió de béns, premis).
2.2 Societats
Tenim quatre tipus principals:
- Societat de responsabilitat limitada
- Societat anònima
- Societat laboral
- Societat cooperativa
Les rendes estan gravades amb l’IS. Altres impostos:
- Impost sobre activitats econòmiques (IAE). Només aplicable a empreses amb facturació superior a 1M.
- Impost sobre el valor afegit (IVA) és aplicable a béns i serveis. Pot tenir dos règims:
- General: tenim el suportat i el repercutit, i la diferència s’ajusta amb l’agència tributària. Diferents tipus: general, reduit i superreduit (21, 10 i 4%).
- Especial: per a empreses detallistes, especialment agricultura, ramaderia i pesca.
- Impost de béns immobles (IBI)
- Impost sobre vehicles de tracció mecànica
- Impost sobre construccions, instal·lacions i obres.
- Impost sobre transmissions patrimonials i actes jurídics documentats.
3. Constitució
La constitució dota de personalitat jurídica a l’empresa, sent susceptible de drets i obligacions. A continuació, es mostren els tràmits necessaris segons la forma jurídica:
- Empresari individual:
- DNI del promotor
- Comunitat de béns i societat civil:
- DNI dels promotors
- Contracte públic o privat de constitució
- Sol·licitud del número d’identificació fiscal (NIF)
- Impost sobre transmissions patrimonials
- Societats mercantils:
- Sol·licitud de certificació negativa del nom o raó social
- Sol·licitud de qualificació per societats laborals i cooperatives
- Justificació d’aportacions dinerades o no dinerades
- Escriptura pública de constitució
- Sol·licitud del número d’identificació fiscal (NIF)
- Liquidació de l’impost de transmissions patrimonials i actes jurídics documentats
- Inscripció en el registre corresponent
4. Posada en marxa
La posada en marxa és posterior a la constitució, i cal per tal de tenir activitat. Són, principalment:
- Alta en el cens d’empresaris i professionals. Es fa mitjançant el model 036 o el 037 (simplificat), i pot incloure el règim especial de l’IVA escollit.
- Alta en l’impost sobre activitats econòmiques. No implica pagament si no se supera 1M d’euros de facturació, i llavors es gestiona al 036.
- Diligencia i legalització dels llibres obligatoris.
- Empresaris individuals
- Estimació directa normal
- Activitat industrial, comercial o de servei: llibre diari, llibre d’inventari i comptes anuals
- Activitat no mercantil: llibre de vendes i ingressos, llibre d’ingressos i despeses i llibre registre de béns d’inversió
- Estimació directa simplificada: factures numerades per data i agrupades per trimestres, llibre de registre de béns d’inversió, llibre de registre de vendes i ingressos, llibre de registre de compres i despeses
- Estimació objectiva: justificants de les operacions.
- Estimació directa normal
- Societats: llibre diari, llibre d’inventaris i comptes anuals, llibre d’actes, llibre d’accions nominatives (SAs) i llibre de registre de socis (SLs)
- Cooperatives: llibre de registre de socis, llibre de registre d’aportacions al capital, llibre d’actes, llibre d’inventari, llibre diari
- Empresaris individuals
- Inscripció de l’empresa en la Seguretat Social, sempre que calgui contractar personal (TA.6)
- Alta de l’empresari en el RETA
- Afiliació i alta dels treballadors (TA.1)
- Compra de locals: cal comprovar la qualificació urbanística de terrenys o locals, i demanar les llicéncies municipals corresponents. Si hi ha una compra, cal formalitzar-la mitjançant un contracte de compravenda (amb escriptura pública al Registre de la Propietat). També, cal donar-se d’alta a l’IBI.
- Arrendament de locals: contractes (verbals o escrits), regulats per la llei d’arrendaments urbans.
- Sol·licitud de llicència d’obres d’acord a la normativa urbanística municipal. Objecte: nova planta, reformes d’edificacions o obres menors.
- Sol·licitud de llicència d’apertura
- Comunicació d’apertura de centre de treball
5. Tramitació
Aquests són els organismes i els tràmits que gestionen:
- Ajuntaments: llicència d’obres, apertura i altres tributs
- Delegació o administració d’hisenda: alta IAE, alta cens d’etiquetes i opcions IVA, alta impost sobre béns immobles, alta estimació objectiva o directa, legalització de llibres obligatoris, obtenció del NIF
- Tresoreria General de la Seguretat Social: inscripció al règim d’autònoms, inscripció de l’empresa, afiliació i alta de treballadors
- Direcció provincial de Treball i Seguretat Social: comunicació d’apertura del centre de treball, segellar el calendari laboral
- Oficines de Treball: registre dels contractes formalitzats amb treballadors, comunicació de contractacions que no requereixen contracte
- Direcció Provincial d’Indústria: inscripció en el registre de la propietat industrial
La finestreta única empresarial (VUE) és una iniciativa que permet la creació d’empreses de forma més fàcil:
- Informa i orienta a l’emprenedor
- Facilita la tramitació a tots els organismes des d’un sol lloc físic
El procés telemàtic es pot fer dirigint-se als punts d’atenció a l’emprenedor (PUE). Tot es gestiona mitjançant un únic document, el document únic electrònic (DUE).
6. Altres aspectes
- Tingueu present les normes de seguretat i higiene en el treball i la prevenció de riscos laborals.
- S’ha de preveure la possibilitat de protegir el vostre producte, nom o marca d’alguna manera.
- Si el local no és de la vostra propietat, haureu de formalitzar un contracte de lloguer.
- Si heu escollit el sistema de franquícia, haureu de signar el contracte que us lligarà amb el franquiciador.
Pla Econòmic i Financer
1. Introducció
El pla econòmic i financer esdevé la síntesi dels aspectes econòmics del pla d’empresa, les magnituds bàsiques del qual s’obtenen a partir dels diversos apartats del pla. Per tant, per ser coherent és necessari que les dades coincideixin amb les obtingudes en cada un d’aquests apartats. Així, per exemple, de les previsions de vendes i dels preus definits se’n desprendran els ingressos previsibles de l’empresa.
Una despesa és una compra d’un bé o servei. Per exemple: matèries primeres, salaris, lloguers, manteniment i neteja, publicitat, consum, assegurances, constitució de l’empresa, etc.
Una inversió és el conjunt de béns que l’empresa adquireix, com màquines i eines, per obtenir el producte o servei, i que mai es vendran.
El pla financer, sempre que es compleixin les circumstàncies previstes en el pla, evidenciarà la viabilitat o no del projecte de negoci.
En aquest pla fóra també interessant, si es tenen els coneixements adients, realitzar un estudi de la viabilitat de la inversió del projecte a través del càlcul del valor actual net (VAN), de la taxa interna de rendibilitat (TIR) i el criteri del pay-back o termini de devolució. També resulta interessant el càlcul d’algunes ràtios com la de rendibilitat, de solvència, d’endeutament, de liquiditat i d’altres.
2. Pla d’inversions i de necessitats inicials
Representa el càlcul de la inversió inicial, així com de la forma de titularitat dels actius de l’empresa. Inclourà el desemborsament necessari per finançar tant l’immobilitzat material (local, maquinària, mobiliari i utillatge, equips informàtics i d’altres) com l’immaterial (despeses de constitució, de primer establiment, drets de traspàs i d’altres) i les existències necessàries per cobrir l’estoc inicial. També cal incloure un estoc de tresoreria per fer front a pagaments inicials, i això permetrà alleugerir les tensions de tresoreria durant els primers mesos d’activitat.
3. Pla de finançament
El finançament de la inversió inicial i de les altres necessitats podrà fer-se mitjançant finançament extern o bé mitjançant finançament propi. Aquest finançament haurà de tenir en compte el finançament del fons de maniobra necessari per al desenvolupament de l’activitat normal de l’empresa. Es tracta que el total de recursos sigui igual al total de les necessitats estimades. Opcions:
- Aportacions pròpies i de socis temporals.
- Lloguer de béns i equips (en lloc de comprar-ne). Renting i leasing.
- Préstecs.
- Ajuts públics i subvencions.
Després, podem comptar amb l’autofinançament associat a les reserves, o bé pel manteniment, com són amortitzacions i provisions. El dia a dia també el podem gestionar amb eines com el confirming, els comptes de crèdit, descobert bancari, descomptes comercials, factoring, canvis de termini a proveïdors i descomptes de pagament immediat.
4. Pla de tresoreria
Permet observar la liquiditat de l’empresa i, per tant, preveure les necessitats de tresoreria que puguin sorgir, mitjançant l’especificació de les entrades i les sortides. Es fa una anotació mes a mes de les entrades i les sortides. També cal detallar les hipòtesis considerades per al seu càlcul, considerant la possible estacionalitat de l’activitat. Això permetrà preveure a temps les mesures adients per solucionar els desequilibris de caixa que se’n puguin derivar.
Per exemple, per a les entrades podríem especificar: aportacions dels socis, préstecs, vendes, interessos dels comptes.
Per les sortides: devolució de préstecs, lloguers, compra de maquinària, assegurances, publicitat, compra de mercaderies, salaris, seguretat social, impostos, subministraments.
Per cada mes, mostrem la diferència entre entrades i sortides, per saber quants diners tindrem al compte bancari.
5. Compte de resultats
Mostra els beneficis o pèrdues esperats per l’empresa com a diferència entre ingressos i despeses. És important especificar tant les diferents partides que determinen el compte de resultats com la seva tendència i evolució, destacant com poden afectar els canvis al resultat global de l’empresa.
És interessant fer totes aquestes previsions considerant diferents escenaris (negatiu, positiu, òptim) per poder estar preparats davant de les diferents necessitats que requerirà cadascun d’aquests casos.
5.1 Amortitzacions
Una amortització és una part d’una despesa més gran, respecte a un element que ens ha de durar un cert temps. Això es fa amb les inversions, per exemple, una maquinària: si ens dura deu anys, l’amortització és la desena part del cost que comptabilitzem anualment.
5.2 Estructura del compte de resultats
A una empresa tenim els comptes d’explotació, que es relacionen amb la seva activitat econòmica habitual, i els resultats financers, relacionats amb l’activitat financera. Trobem els següents apartats:
- Ingressos d’explotació: vendes.
- Despeses d’explotació: lloguers, amortitzacions, assegurances, publicitat, compra de mercaderies, salaris, seguretat social, impostos, subministraments.
- Ingressos financers: interessos bancaris.
- Despeses financeres: interessos del préstec.
Això ens permet calcular el resultat d’explotació (o benefici abans d’interessos o impostos) i el resultat financer. Si els sumem, tenim el resultat abans d’impostos. Si li descomptem els impostos, tenim el resultat de l’exercici (o benefici net).
Si els resultats són negatius, hem de fixar-nos si els resultats d’explotació són positius o negatius. Si són positius, el problema és que encara tenim resultats financers negatius, que es considera normal especialment si estem començant.
5.3 Regles per l’elaboració
Aquestes serien els principis a seguir per a l’elaboració:
- És despesa tot allò que l’empresa compra i consumeix.
- Per a compres i vendes, apliquem el criteri de meritació: es considera despesa quan s’ha generat, encara que no s’hagi fet el pagament, i els ingressos quan es meritin, encara que no s’hagin percebut encara.
- Tots aquells diners que entren a la nostra empresa i hem de retornar no es consideraran ingressos, i viceversa: els que surten i han de tornar-nos no es consideren despesa.
El Pla General Comptable, a més d’aquests principis, identifica uns altres: empresa en funcionament, uniformitat, no compensació i importància relativa.
6. El balanç
El balanç és la representació comptable del patrimoni de l’empresa.
El patrimoni d’una empresa es compon de:
- El conjunt de béns: tot allò que pertany a l’empresa, com maquinària, locals, vehicles, mobiliari, patents, etc.
- El conjunt de drets: tot allò que se li deu a l’empresa, com factures dels seus clients, lletres de canvi per pagar, etc.
- El conjunt d’obligacions: tot allò que l’empresa deu, com els préstecs demanats al banc, lletres de canvi que es deuen a proveedors, etc.
En tot balanç es compleix sempre que actiu = passiu + patrimoni net.
6.1 Actius
Són els béns que posseeix l’empresa. Tenim la següent classificació:
- Actiu no corrent (de llarg termini)
- Immobilitzat intangible: aplicacions informàtiques, drets de traspàs, despeses d’investigació.
- Immobilitzat material: construccions, eines, maquinària.
- Inversions immobiliàries: terrenys o edificis llogats o en venda.
- Immobilitzat financer: accions a altres empreses, bons, fiances.
- Actiu corrent (curt termini: un any com a molt)
- Existències: mercaderies, materies primeres, recanvis.
- Crèdits pendents de cobrament: clients, deudors.
- Efectiu: caixa, banc.
6.2 Passius
Són tot allò que l’empresa deu. Pot ser:
- Passiu no corrent: deutes que cal retornar a llarg termini. Crèdits.
- Passiu corrent: deutes a retornar a curt termini (menys d’un any): pagament a proveïdors.
6.3 Patrimoni net
És la riquesa de l’empresa: capital, reserves, pèrdues i guanys.
7. Balanç final previsional
Al final de l’exercici, hem d’analitzar el balanç.
El passiu ens diu com financem l’empresa, i l’actiu en què invertim o gastem aquests diners. Al començament de l’exercici són iguals, i la seva diferència al final de l’exercici ens diu si hem tingut beneficis o pèrdues.
8. Impostos
8.1. Impostos
Els principals impostos que un emprenedor pot trobar-se són:
* Impostos directes: impost sobre la Renda de les persones Físiques (IRPF), Impost sobre Societats (IS)
* Impostos Indirectes (Impost sobre el Valor Afegit (IVA)
8.2. Impost sobre Societats
L’Impost sobre societats és un impost:
- Directe. Es calcula en funció de la renda obtinguda per la societat.
- Personal. S’aplica a totes les rendes obtingudes pel contribuent.
- Proporcional. S’aplica un tipus de gravamen fix amb independència de la renda.
- Periòdic.
El període impositiu és anual, coincidint amb l’exercici econòmic de cada societat. Si la societat no fixa un període diferent, per defecte, l’any fiscal va de l’1 de gener al 31 de desembre del mateix any. L’impost sobre societats es merita l’últim dia del període impositiu.
L’esquema de liquidació de l’impost parteix del resultat comptable de la societat, sobre el qual es calcula la base imposable a la qual s’aplica el gravamen corresponent. A partir d’aquí, s’hi apliquen les deduccions a les quals tingui dret, obtenint la quota diferencial.
El tipus de gravamen general és del 30%.
Per a les entitats de dimensió reduïda (amb una xifra de negoci inferior als 10 M€) que compleixen unes condicions determinades, el tipus de gravamen és del 25% per la part de la base imposable entre 0 i 300.000 € i del 30% per l’excés.
Pel que fa a les entitats amb una xifra de negoci inferior als 5 M€ i una plantilla de menys de 25 treballadors que tributen al tipus general amb condicions, el tipus de gravamen és del 20% si base imposable és inferior a 300.000 € i del 25% per la resta.
8.3. IRPF
L’impost sobre la renda de les persones físiques és un tribut de caràcter personal i directe que grava, segons els principis d’igualtat, generalitat i progressivitat, la renda de les persones físiques d’acord amb la seva naturalesa i les seves circumstàncies personals i familiars.
8.4. IVA
L’impost sobre el valor afegit (IVA) és un impost indirecte que grava el consum i que recau sobre el lliurament de béns i prestacions de serveis que fan empresaris o professionals, com també sobre les importacions i adquisicions intracomunitàries de béns.
Referències
- Elección de la forma jurídica
- Barcelona Activa Emprenedoria
- Barcelona Activa: Formes jurídiques i tràmits
- Barcelona Activa: Activitats
- Canal Empresa: Constitució i tràmits
- PAE Electrónico
- Estimación objetiva (módulos) 2020
- Simulador de préstec
Pla d’empresa
- Primer checkpoint
- Segon checkpoint
- Tercer checkpoint
- Quart checkpoint
- 1 Pla jurídic
- 2 Pla econòmic financer
- 2.1 Pla d’inversió inicial
- 2.2 Pla de finançament
- 2.3 Simulació del préstec
- 2.4 Sistema de cobrament als clients i sistema de pagament als proveïdors
- 2.5 Explicació del càlcul dels ingressos i la previsió d’ingressos d’explotació
- 2.6 Previsió de despeses d’explotació
- 2.7 Tipus d’IVA repercutit i suportat de l’activitat
- Lliurament final
Aquest és un guió per a confeccionar un pla d’empresa basat en quatre checkpoints.
Primer checkpoint
Aquest checkpoint conté dos punts: el pla estratègic i el canvas.
1 Pla estratègic
1.1 Dades personals de l’equip promotor
- Per a totes les persones implicades en el projecte:
- Nom i cognoms
- DNI
- Data de naixement
- Adreça
- Telèfon
- Formació i experiència laboral relacionada amb les diferents tasques que es desenvoluparan en la nova empresa
- Destacar els elements que estiguin més relacionats amb l’activitat empresarial: formació, experiència relacionades en atenció al públic, experiència en àmbit comercial, en gestió econòmica i administrativa, etc.
- Destacar si tens experiències prèvies en negocis similars o en el cas d’existir algun buit en relació amb algun aspecte comentar com se superaran (Punts forts i punts febles de les capacitats dels emprenedors front l’activitat)
- Aportar el currículum vitae de cadascun dels promotors a l’annex, com a carta de presentació del vostre perfil i de la vostra capacitat tècnica.
Aquesta informació es pot copiar de FOL.
1.2 Motivacions i origen de la idea
- Explicar els motius que es tenen per crear l’empresa.
- Explicar l’evolució de la idea: com va sorgir la idea de crear l’empresa? Com ha anat canviant?
- Quines persones participen en el projecte i quin és el grau d’implicació de cadascuna.
- Projecta l’empresa cap al futur: quina és la vostra visió?
- Explica els valors que voleu que es reconeguin en la vostra activitat.
- Quins criteris socials aplicareu a la vostra empresa? Veure annex: llista de criteris socials.
1.3 Descripció de l’activitat
- Quina és la missió del negoci?
- Tipus d’activitat que es durà a terme. Quin producte o servei es vendrà?
- Expliqueu la vostra proposta de valor. Elements diferencials de la vostra idea. Creativitat i originalitat.
- Petita planificació de l’activitat. Quan comença? On es fa físicament?
Entre l’apartat 1.2 i el 1.3, escriu almenys una pàgina.
2 Canvas
Inclou el canvas del model de negoci desenvolupat.
Segon checkpoint
1 Pla de màrqueting
1.1 Anàlisi de mercat
- Descripció del sector de l’activitat i projecció: descriure el sector econòmic on es desenvoluparà l’activitat empresarial de l’empresa i la previsió futura d’aquest sector de manera global.
- Concretar el mercat escollit i tendències: Definir i descriure el mercat concret al qual es dirigirà l’activitat de l’empresa (zona geogràfica, sectors, etc.), i les tendències o comportament que es preveu en aquest àmbit local.
Fer 1 a 1,5 pàgines.
1.2 Segmentació de Clients
- Determinar el perfil comú dels nostres clients potencials.
- En el cas que existeixin diferents tipus de clients, cal descriure’ls indicant quins criteris s’han utilitzat per diferenciar-los i per què s’han triat aquests criteris. Explicar si són empreses, particulars, altres entitats, administracions, etc.
- Dimensionar el mercat potencial: quants consumidors o clients componen aquests mercats.
Incloure una taula amb una fila per cada tipus de client, amb les següents columnes:
- Perfil demogràfic: gènere, edat, estat civil, mida familiar, ocupació, nivell d’estudis, nivell de renda.
- Perfil psicogràfic: estil de vida, personalitat, classe social.
- Comportament: benefici buscat, freqüència de compra, nivell de lleialtat, nivell d’ús, lloc i moment d’ús, categories d’usuaris.
Fer 1 pàgina.
1.3 Anàlisi de la competència
- Característiques de la competència (directa / indirecta): preus, localització, productes/serveis, mida, tipus de client, qualitat, directa o indirecta.
- És directa o indirecta: ofereixen el mateix que la teva empresa o un de similar, però que poden ser considerats com a competència.
- Posicionament de la competència: quina estratègia competitiva utilitza? Què fa i com per atraure clients.
- Punts forts i febles de la competència respecte de l’estàndard del mercat
Pots fer un quadre d’anàlisi de la competència directa. Per cada empresa competidora, i per la teva, afegeix aquestes columnes:
- Nom de l’empresa
- Producte o servei
- Nivell de preus
- Localització
- Punts febles respecte al mercat
- Punts forts respecte al mercat
Fer 1 pàgina.
1.4 Posicionament i avantatge competitiu
Aspectes diferenciadors del nostre producte/servei respecte de la competència directa. T’has de posar en el punt de vista del client final i explicar:
- Què ens diferencia?
- Què influirà en les decisions de compra del possible client?
- Quina necessitat es cobreix?
- Amb quina freqüència compra?
- Si l’usuari o beneficiari és diferent de qui fa la compra, indicar qui decideix la compra i quines necessitats es cobreixen.
Fer mitja pàgina.
1.5 Màrqueting-Mix del projecte
El màrqueting-mix es fa en quatre punts:
- Producte/servei: característiques i elements diferenciadors respecte de l’oferta actual (qualitat, servei postvenda, presentació, segmentació, etc.).
- Política de preus:
- Criteris per determinar-los: segons la competència, segons els costos, etc.
- Llista de preus i previsió de vendes mensual
- Forma de cobrament
- Promoció i publicitat
- Com es donarà a conèixer l’empresa i el seu producte/servei? Mitjans, promocions especials, destí de les accions.
- Cost de la publicitat i/o les accions de promoció. Cost previst, impacte previst en clients i ingressos.
- Distribució i comercialització
- Com arribarà el producte/servei al client? Canals majoristes, detallistes, punts de venda directa, etc.
- Com s’organitzarà la tasca comercial amb relació als canals.
Fer 2 pàgines.
2 Taula financera
Fer una taula esborrany anotant les despeses (fixes i variables) i els ingressos que es produeixen al teu negoci quan ja es troba en un punt d’equilibri financer. Pots assumir que això passa després d’un cert temps de funcionament, com per exemple un any.
3 Canvas (actualitzat)
Inclou l’actualització del canvas del model de negoci amb relació als aspectes desenvolupats en aquest segon checkpoint.
Tercer checkpoint
1 Pla de màrqueting en línia
1.1 Objectius
Expliqueu els vostres objectius de màrqueting. Han de ser (SMART): eSpecífic, Mesurable (analítica web), Assolible, orientat a Resultats, basat en un Temps delimitat.
1.2 Estratègia inbound
- Fes un estudi de les paraules clau del teu negoci:
- Indica les paraules clau escollides.
- Justificar-les.
- Explica on juga la teva competència.
- Defineix i crea el teu contingut SEO.
- Fes un diagrama amb la jerarquia, el títol i les paraules clau per a cada pàgina.
- Fes un esbós del text de la pàgina principal, on hi ha d’haver les paraules clau.
- Escriu el teu calendari editorial: un full de càlcul o taula amb les publicacions que preveus fer els tres primers mesos, aproximadament.
1.3 Estratègia outbound
Defineix una estratègia de màrqueting outbound per al teu negoci. Indica la teva inversió a cercadors, xarxes socials o altres mitjans.
1.4 Mesurament
Defineix almenys cinc indicadors KPI en relació als teus objectius i explica com faràs el seu mesurament.
Total: 3 pàgines (1/2, 1 i 1/2, 1/2 i 1/2).
2 Pla operacional
L’objectiu és detallar com es fabricarà el producte o quin és el procés de prestació del servei. Cal que descriguis totes les depeses que faràs.
- Recursos infraestructurals: descripció del local, ubicació, entorn, què cal fer per adequar-lo i les despeses necessàries de lloguer.
- Recursos materials: maquinària, eines, mobiliari, matèries primeres.
- Recursos humans: funcions i tasques necessàries, descriure els perfils professionals, dedicació horària, procediment de selecció i costos salarials.
- Proveïdors:
- Llista de proveïdors i forma de pagament
- Contractacions externes
- Serveis i subministraments: despeses de gestor, subministraments, comunicacions, manteniment, reparacions, transport, dietes, assegurança de responsabilitat civil, vehicles.
De 1 ½ a 2 pàgines.
3 DAFO
L’anàlisi DAFO ens ajuda a dissenyar la nostra estratègia mitjançant l’avaluació de quatre aspectes, classificables en interns/externs i positius/negatius:
- Amb origen intern, tenim fortaleses (positives) i debilitats (negatives).
- Amb origen extern, tenim oportunitats (positives) i amenaces (negatives).
Fortaleses: visuals i mostrats o ocults .. Què et fa diferent? Què destaca?
- De què ets bo de forma natural?
- Quines habilitats heu treballat per desenvolupar?
- Quins són els vostres talents o els regals naturals?
- Què tan forta és la vostra xarxa de connexions?
- Què veuen les altres persones com a punts forts?
- Quins valors i ètica us distingeixen dels companys?
Debilitats: coses a millorar. Obstacles a la vostra vida / carrera professional.
- Hàbits i trets negatius de treball.
- Educació / formació que necessita millorar.
- Què consideren els altres com a debilitats?
- On es pot millorar?
- Què tens por de fer o probablement evites? Què t’espanta? Falta confiança per fer-ho?
Oportunitats: factors externs per aprofitar per dur a terme els vostres somnis.
- Estat de l’economia.
- Noves tecnologies o temes a tractar.
- Demanda de l’habilitat o el tret que posseeix.
- Hi ha la necessitat que ningú compleixi la seva carrera professional / lloc de treball / indústria?
- Esteu en una indústria en creixement?
- Podeu assistir a xarxa, classes o conferències?
- Algun dels seus punts forts us ofereix una oportunitat?
- Alguna de les seves debilitats es convertiria en una oportunitat si es supera?
Amenaces: factors externs que poden fer mal o disminuir les possibilitats d’assolir els vostres objectius.
- Alguna de les seves debilitats inhibeixen la capacitat de créixer / augmentar / millorar (company / lfe)?
- La vostra indústria contracta o canvia / canvia d’orientació?
- Hi ha una forta competència per als llocs de treball que més s’adapten?
- Quin és el perill extern més gran dels vostres objectius?
- Hi ha noves normes professionals que no pugui complir?
- Hi ha nous requisits tecnològics / educatius o de certificació que impedeixin el vostre progrés?
Relaciona els punts forts amb les oportunitats per saber on centrar-se i prendre mesures.
Compondre amb debilitats i amenaces et mostra on treballar i millorar.
Cerqueu qualsevol manera de convertir les vostres debilitats o amenaces en oportunitats o punts forts.
1 pàgina.
4 Canvas (actualitzat)
5 Taula financera (actualitzada)
Al checkpoint anterior vas descriure les despeses. Descriu ara la previsió de vendes anuals dels tres primers exercicis. Quina seria la previsió de vendes mensual i diària per al primer exercici? (fes la divisió)
Quart checkpoint
1 Pla jurídic
Prendre com a referència el pla d’empresa Vapor Llonch.
Fer 2 pàgines.
1.1 Forma jurídica
Indicar la forma jurídica escollida i motius de l’elecció.
1.2 Tràmits
Elaborar una llista dels tràmits i els costos pertinents.
1.3 Obligacions fiscals
Descriure les obligacions pròpies de la forma jurídica escollida.
1.4 Obligacions comptables
Descriure les obligacions laborals que tindrà l’empresa, així com el règim de la seguretat social al que s’acolliran els socis i la contractació prevista, i així mateix, calcular el cost laboral.
1.5 Seguretat social
Altres aspectes a destacar en relació amb l’activitat: prevenció de riscos laborals, qualitat, medi ambient, etc.
2 Pla econòmic financer
Prendre com a referència el pla econòmic i financer autònom (punt 3).
Fer 2 pàgines i 1/2.
2.1 Pla d’inversió inicial
Detalla al màxim l’explicació de la inversió per a dur a terme el projecte, a més d’omplir el requadre de la plantilla.
2.2 Pla de finançament
Detallar al màxim l’explicació del pla de finançament (% recursos propis, aportació dels socis, nombre de socis, endeutament dels socis, condicions del préstec, etc.). Omple el requadre de la plantilla.
2.3 Simulació del préstec
Utilitza un simulador per omplir el préstec de la plantilla. A internet trobaràs diferents simuladors.
2.4 Sistema de cobrament als clients i sistema de pagament als proveïdors
Detallar el sistema de cobrament dels clients i pagament als proveïdors (contat o diferit en el temps).
2.5 Explicació del càlcul dels ingressos i la previsió d’ingressos d’explotació
Molt important, a més de quantificar els ingressos, és detallar com s’ha realitzat el càlcul dels ingressos. Utilitza la taula de la plantilla.
2.6 Previsió de despeses d’explotació
Quantifica les despeses d’explotació i calcula el resultat d’explotació. Utilitza la taula de la plantilla.
2.7 Tipus d’IVA repercutit i suportat de l’activitat
Explicació del tipus d’IVA suportat i repercutit, així com del sistema tributari de l’IVA (estimació directa, indirecta, etc.).
Lliurament final
1 Els quatre checkpoints revisats
Cal incloure tots els comentaris fets pel professor durant els quatre checkpoints, i totes les actualitzacions que calguin en funció del desenvolupament del pla.
2 Conclusions
La conclusió és l’apartat on fareu una valoració del procés de planificació d’aquesta primera idea de negoci que vàreu plantejar a l’inici. També es poden remarcar quins entrebancs heu trobat durant l’elaboració del pla i com els heu donat solució.
És aquí on senyalareu els canvis que ha experimentat la idea inicial i quins són els punts remarcables del pla. Podeu fer una projecció de futur del vostre model de negoci i de quins són els objectius que voldríeu assolir. Cal finalment agrair atots aquells que han fet possible que la vostra tasca arribés a bon port.
3 Maquetació
Tapa, contratapa, índex i paginació.
