Tenim el llenguatge (07-01), les llibreries base (07-02) i el mapa d'eines (07-03). Falta el que converteix tot això en un taller en què diverses persones puguin treballar avui i d'aquí a un any: l'entorn. Als mòduls 3 a 6 hem escrit scripts solts, amb un fitxer novamarket_ml.py del qual importàvem funcions, sense dir mai quina versió de Python o de scikit-learn calia, on es desava cada cosa ni com comprovar que continuava funcionant. Aquesta lliçó ho ordena: entorns virtuals i gestió de dependències (venv, pip, requirements.txt, conda, uv/poetry) i per què fixar versions és part de la reproduïbilitat, juntament amb les llavors; Jupyter amb les seves virtuts i les seves trampes (l'estat ocult); Colab i Kaggle com a laboratoris amb GPU gratuïta; els IDE (VS Code, PyCharm); Git aplicat a projectes d'IA (què es versiona i què no); una estructura de projecte recomanada que aplicarem de debò: refactoritzarem el codi de NovaMarket en un paquet src/novamarket/ amb dades.py, entrenar.py i tests amb pytest, i ho executarem; i, per tancar, quan cal GPU o núvol i què és Docker. És important perquè el projecte que la Marta i el Diego abordaran al mòdul 8 no cap en un notebook: necessita aquesta estructura per sobreviure al primer canvi de persona, de màquina o de versió.
Contingut
- El problema: "a la meva màquina funciona"
- Entorns virtuals amb
venvi dependències ambpip requirements.txt, conda/mamba,uvi poetry- Reproduïbilitat: versions + llavors
- Jupyter Notebook i JupyterLab: cel·les, kernel i estat ocult
- Google Colab i Kaggle Notebooks
- IDE: VS Code i PyCharm
- Git per a projectes d'IA: què versionar i què no
- Estructura de projecte recomanada
- Codi: refactoritzar NovaMarket a
src/novamarket/amb tests - GPU, CPU i núvol: quan cal
- Docker com a entorn reproduïble
- Errors Comuns i Consells
- Exercicis
- Conclusió
- El problema: "a la meva màquina funciona"
La Marta entrena el predictor de devolucions al seu portàtil, obté AUC 0,844 i passa l'script a un company. A ell li falla en importar, o li dona un altre número, o el joblib no carrega. Les causes són sempre les mateixes: una altra versió de Python o d'una llibreria, un altre ordre d'execució de les cel·les del notebook, una altra llavor o cap, dades diferents en un fitxer amb el mateix nom. L'entorn de desenvolupament és el conjunt de decisions que elimina aquestes causes una a una: aïllar dependències, fixar versions, fixar llavors, ordenar el codi, versionar-lo, provar-lo i, quan cal, empaquetar la màquina sencera.
- Entorns virtuals amb
venv i dependències amb pip
venv i dependències amb pipUn entorn virtual és una carpeta amb el seu propi intèrpret de Python (enllaçat al del sistema) i la seva pròpia col·lecció de paquets, aïllada de les altres. Cada projecte té el seu, i així el projecte A pot fer servir una versió de pandas i el B una altra sense trepitjar-se. venv ve amb Python:
# A la carpeta del projecte (Linux/macOS; a Windows: .venv\Scripts\activate)
python3 -m venv .venv # crea la carpeta .venv amb un Python net
source .venv/bin/activate # activa: a partir d'aquí, "python" i "pip" són els de l'entorn
python -c "import sys; print(sys.executable)" # .../.venv/bin/python
pip install numpy pandas scikit-learn matplotlib torch jupyterlab pytest # instal·la NOMÉS a l'entorn
pip list # què hi ha instal·lat i en quina versió
deactivate # torna al Python del sistemapip instal·la paquets des de PyPI (l'índex públic de Python) resolent dependències: en demanar scikit-learn porta NumPy, SciPy, joblib i threadpoolctl. La regla d'or: no instal·lis mai llibreries al Python del sistema; sempre en un entorn del projecte. L'entorn del curs és exactament això: un venv amb NumPy, pandas, scikit-learn, Matplotlib, PyTorch (CPU) i, des d'aquesta lliçó, pytest.
requirements.txt, conda/mamba, uv i poetry
requirements.txt, conda/mamba, uv i poetryPerquè una altra persona (o tu d'aquí a un any) recreï l'entorn, s'escriu la llista de paquets amb versions en un fitxer:
# requirements.txt del projecte NovaMarket (les versions són les de l'entorn del curs en escriure això;
# al teu projecte, les que facis servir tu)
numpy==2.5.2
pandas==3.0.5
scikit-learn==1.9.0
matplotlib==3.11.1
torch==2.13.0
joblib==1.5.3
jupyterlab
pytestpip install -r requirements.txt # instal·la exactament això
pip freeze > requirements-lock.txt # bolca TOT el que hi ha instal·lat amb versió exacta (dependències incloses)Convenció habitual: un requirements.txt amb el que tu decideixes (paquets directes, fixats amb == els que importen) i un fitxer de bloqueig generat amb freeze per reproduir al bit. Alternatives que veuràs als equips:
| Eina | Què hi afegeix | Fitxer | Quan |
|---|---|---|---|
venv + pip |
Res: l'estàndard, sense instal·lar | requirements.txt |
Per defecte; suficient per a gairebé tot |
| conda / mamba | Gestiona també Python i llibreries no Python (CUDA, compiladors, GDAL); entorns per nom; mamba és conda ràpid | environment.yml |
Ciència de dades amb dependències natives, GPU, Windows |
uv |
Instal·lador i gestor molt ràpid (escrit en Rust), crea entorns, bloqueja versions, gestiona versions de Python | pyproject.toml + uv.lock |
Projectes nous que volen velocitat i bloqueig rigorós |
| poetry | Gestió de dependències i empaquetament amb bloqueig | pyproject.toml + poetry.lock |
Llibreries i projectes que es publiquen |
| pyenv | Només instal·la i canvia versions de Python | — | Quan necessites diverses versions de Python |
Un environment.yml de conda equivalent, il·lustratiu:
name: novamarket
channels: [conda-forge]
dependencies:
- python=3.12
- numpy
- pandas
- scikit-learn
- matplotlib
- pytorch-cpu
- jupyterlab
- pytestconda env create -f environment.yml i conda activate novamarket. Tria una eina per projecte i no les barregis.
- Reproduïbilitat: versions + llavors
Un resultat és reproduïble si una altra persona obté el mateix número. Calen dues coses que hem separat al curs i que ara ajuntem:
- Llavors:
np.random.default_rng(42),random_state=42a scikit-learn,torch.manual_seed(42), ishuffleamb llavor atrain_test_splitiDataLoader. Sense elles, les 3.000 comandes, la partició i la inicialització de la xarxa canvien a cada execució. (En GPU, algunes operacions són no deterministes encara que fixis la llavor; PyTorch documenta com forçar-ho a canvi de velocitat.) - Versions: la mateixa llavor en una altra versió de NumPy o de scikit-learn pot donar una altra seqüència o un altre resultat numèric, i un
joblibdesat amb una versió pot no carregar amb la següent (07-01, 07-03). Per això elrequirements.txtamb==i per això, a la secció 10, l'script d'entrenament desarà al costat del model un JSON amb la versió de Python i de scikit-learn amb què es va entrenar.
Reproduïble = codi versionat + dades identificades + versions fixades + llavors fixades. Les quatre; amb tres, el número canvia.
- Jupyter Notebook i JupyterLab: cel·les, kernel i estat ocult
Jupyter és l'entorn interactiu per excel·lència de la ciència de dades: un document (.ipynb) amb cel·les de codi i de text (Markdown), que s'executen una a una contra un kernel (un procés Python viu que guarda les variables). JupyterLab és la interfície moderna amb pestanyes, explorador de fitxers i terminal. Es llança amb jupyter lab des de l'entorn activat, i obre el navegador. Les seves virtuts són evidents: veus el head() i el gràfic al costat del codi, iteres ràpid, documentes mentre explores. Les seves trampes vénen del mateix:
- Estat ocult: les variables viuen al kernel, no al document. Si executes la cel·la 5, després la 2 i després edites i executes la 5 una altra vegada, el notebook que veus no correspon a l'estat del kernel. El cas típic: esborres la cel·la que definia
comandesi tot continua funcionant... fins que reinicies, i llavorsNameError. Un altre: canviestest_sizea la cel·la 3 però no reexecutes la 4, i l'AUC que veus és l'antic. - Ordre d'execució: els números entre claudàtors
[7]a l'esquerra de cada cel·la diuen l'ordre real; si no van de dalt a baix, desconfia. - Bones pràctiques que eviten gairebé tots els disgustos: (1) abans de donar per bo un resultat, "Restart Kernel and Run All" (reiniciar i executar-ho tot de dalt a baix; si falla, el notebook mentia); (2) importacions i paràmetres a les primeres cel·les; (3) no deixar cel·les de prova desordenades: esborra-les o mou-les a un apèndix; (4) funcions que es repeteixen, a un mòdul
.pydel projecte (secció 9) i importar-les, no copiar-les de notebook en notebook; (5) exportar a script quan el codi madura:jupyter nbconvert --to script exploracio.ipynbgeneraexploracio.py, o millor, copiar a mà el que val al mòdul; (6) netejar sortides abans de pujar a Git (els.ipynbamb sortides guarden imatges i taules i fan il·legibles les diferències):jupyter nbconvert --clear-output --inplace, o eines comnbstripout. - Un notebook és per explorar i explicar; el codi que s'executa cada dia (entrenar, servir) va en mòduls i scripts. Aquesta frontera és la que traçarem a la secció 10.
- Google Colab i Kaggle Notebooks
Quan no tens GPU, o vols compartir un notebook sense que ningú instal·li res, hi ha dos laboratoris gratuïts al navegador:
- Google Colab: notebooks Jupyter allotjats per Google, amb Python i les llibreries habituals preinstal·lades, i accés a GPU (i TPU) gratuïta amb límits de temps de sessió, d'hores per dia i de memòria que canvien i que a la versió gratuïta no es garanteixen; les versions de pagament amplien el contingent. La sessió és efímera: en tancar-se es perd el que hi hagi al disc, així que les dades es pugen a cada sessió o es llegeixen de Google Drive.
- Kaggle Notebooks: semblant, integrat amb els conjunts de dades i competicions de Kaggle, amb un contingent setmanal d'hores de GPU i molts notebooks públics dels quals aprendre.
- Com pujar un CSV (descripció, no executable aquí): a Colab, el panell lateral de fitxers té un botó de pujada, o des de codi
from google.colab import files; files.upload()obre un diàleg; per a fitxers grans, muntar Drive ambdrive.mount('/content/drive')i llegir ambpd.read_csv('/content/drive/MyDrive/comandes.csv'). A Kaggle, "Add Data" afegeix un dataset (propi o públic) que apareix sota/kaggle/input/. - Cauteles: no pugis dades personals de clients reals a un servei extern sense l'avaluació de 02-04; fixa versions perquè l'entorn preinstal·lat canvia; i desa el notebook i els models fora de la sessió (Drive, Git) perquè desapareixen.
- IDE: VS Code i PyCharm
Un editor amb suport de Python multiplica la productivitat davant del notebook per al codi de mòduls:
- Visual Studio Code amb les extensions Python i Jupyter: autocompleció, navegació al codi, formatatge, execució de notebooks dins de l'editor i "cel·les" en fitxers
.pyamb# %%(el millor de tots dos mons: script versionable que s'executa per trossos), depurador amb punts d'interrupció (molt millor que sembrarprintper veure per què elColumnTransformerdona 22 columnes en lloc de 21), terminal integrat, Git integrat, i selecció de l'intèrpret de l'entorn virtual (Python: Select Interpreter→.venv). - PyCharm: l'IDE de JetBrains, més "tot inclòs" (l'edició Community és gratuïta; la Professional hi afegeix suport científic i notebooks). Excel·lent refactorització i depuració.
- Altres: Spyder (interfície tipus MATLAB/RStudio, amb conda), Positron, o qualsevol editor amb servidor de llenguatge. L'elecció és personal; el que no és opcional és fer servir l'intèrpret de l'entorn del projecte i tenir a mà depurador i tests.
- Git per a projectes d'IA: què versionar i què no
Git guarda la història del codi i permet treballar a diverses persones sense trepitjar-se; GitHub/GitLab l'allotgen. En un projecte d'IA hi ha una particularitat: les dades i els models són grans, canvien per raons diferents del codi i de vegades són confidencials. La regla:
| Es versiona a Git | No es versiona a Git (s'ignora amb .gitignore) |
|---|---|
Codi (src/, tests/, scripts) |
Dades crues i processades (data/) |
Fitxers de dependències (requirements.txt, pyproject.toml) |
Models entrenats grans (models/*.joblib, *.pt) |
| Notebooks sense sortides o amb sortides lleugeres | Entorns virtuals (.venv/), memòries cau (__pycache__/, .ipynb_checkpoints/) |
| Metadades petites: mètriques en JSON, configuració | Secrets: claus d'API, contrasenyes (.env) |
README.md, documentació |
Fitxers generats que es recreen amb el codi |
Per a dades i models es fan servir DVC (Data Version Control: desa a Git un fitxer petit amb el hash de la dada i la dada en un emmagatzematge extern, S3, Drive, un disc de xarxa) o Git LFS (Large File Storage). Només els esmentem: per a NovaMarket començarem amb .gitignore i una carpeta compartida, i DVC quan calgui.
Flux mínim:
git init # una vegada, a l'arrel del projecte
git add . # prepara els fitxers (els ignorats no hi entren)
git commit -m "Estructura del projecte i mòdul de dades"
git status # què ha canviat
git log --oneline # història
git checkout -b experiment-boosting # una branca per experiment o funcionalitat
- Estructura de projecte recomanada
Dels scripts solts dels mòduls 3-6 a un projecte que una altra persona entén en un minut. Estructura per a NovaMarket:
flowchart TB
R["novamarket_ia/"] --> RM["README.md<br/>què és, com instal·lar, com executar"]
R --> RQ["requirements.txt / pyproject.toml<br/>dependències amb versió"]
R --> GI[".gitignore"]
R --> D["data/<br/>raw/ (tal com arriben)<br/>processed/ (netes) — NO a Git"]
R --> NB["notebooks/<br/>01_exploracio_comandes.ipynb<br/>02_model_devolucions.ipynb"]
R --> S["src/novamarket/<br/>__init__.py<br/>dades.py<br/>entrenar.py<br/>predir.py"]
R --> M["models/<br/>model_devolucions.joblib (ignorat)<br/>model_devolucions.json (mètriques, versions)"]
R --> T["tests/<br/>test_dades.py<br/>test_entrenar.py"]
Principis: data/raw/ és intocable (el que arriba es desa tal qual; tota la resta es recalcula amb codi); els notebooks es numeren i expliquen una història; el codi reutilitzable viu al paquet src/novamarket/ (la carpeta src/ evita que Python importi per accident una còpia sense instal·lar); models/ guarda artefactes amb les seves mètriques al costat; tests/ comprova el que no es pot trencar; i README.md diu com arrencar. Existeixen plantilles (Cookiecutter Data Science és la més coneguda) que generen això amb més carpetes (reports/, configs/, docs/); comença petit i afegeix-ne quan ho necessitis.
- Codi: refactoritzar NovaMarket a
src/novamarket/ amb tests
src/novamarket/ amb testsHo muntarem de debò. Creem l'estructura, movem les funcions de novamarket_ml.py (mòduls 4-5) a src/novamarket/dades.py, escrivim entrenar.py com a script executable, un test mínim amb pytest i ho executem tot.
1. Crear l'estructura (en un terminal, amb l'entorn activat):
mkdir -p novamarket_ia/{data/raw,data/processed,notebooks,src/novamarket,models,tests}
cd novamarket_ia
touch data/raw/.gitkeep data/processed/.gitkeep models/.gitkeep # perquè Git conservi carpetes buides2. src/novamarket/__init__.py (converteix la carpeta en paquet):
3. src/novamarket/dades.py: les funcions que ja coneixes, sense canvis, amb una capçalera. Reproduïm la primera completa; embrutar_comandes, preparar_comandes i crear_preparacio (amb les seves llistes NUMERIQUES, BINARIES, NOMINALS, ORDINALS) es copien tal qual de 04-03:
"""Dades de NovaMarket: generació sintètica (mòdul 4), neteja (04-03) i preparació (ColumnTransformer)."""
import numpy as np
import pandas as pd
def generar_comandes_ml(n=3000, llavor=42):
"""Genera n comandes fictícies de NovaMarket amb l'etiqueta 'retornat' (1 = retornada)."""
rng = np.random.default_rng(llavor)
import_comanda = np.round(rng.gamma(shape=2.0, scale=60.0, size=n) + 5, 2)
num_articles = rng.integers(1, 6, size=n)
dies_lliurament = rng.integers(1, 8, size=n)
client_nou = rng.random(n) < 0.30
categoria = rng.choice(["electronica", "llar", "informatica", "accessoris"],
size=n, p=[0.35, 0.30, 0.20, 0.15])
zona = rng.choice(["A", "B", "C"], size=n, p=[0.40, 0.35, 0.25])
z = (-3.4 + 0.010 * (import_comanda - 100) + 1.6 * client_nou + 0.35 * (dies_lliurament - 4)
+ 1.0 * (categoria == "electronica") + 0.5 * (categoria == "informatica")
- 0.2 * (num_articles - 2) + 1.2 * client_nou * (import_comanda - 100) / 100)
prob = 1 / (1 + np.exp(-z))
retornat = (rng.random(n) < prob).astype(int)
return pd.DataFrame({"import_comanda": import_comanda, "num_articles": num_articles, "dies_lliurament": dies_lliurament,
"client_nou": client_nou.astype(int), "categoria": categoria,
"codi_postal_zona": zona, "retornat": retornat})
def generar_demanda_setmanal(setmanes=104, llavor=42): ... # idèntica a 04-02
def embrutar_comandes(comandes, llavor=42): ... # idèntica a 04-03
def preparar_comandes(brut): ... # idèntica a 04-03: retorna X, y
def crear_preparacio(): ... # idèntica a 04-03: ColumnTransformer -> 21 columnes4. src/novamarket/entrenar.py: funcions importables i script executable, separats per if __name__ == "__main__":. Al costat del model desa un JSON amb mètrica, paràmetres i versions (secció 4):
"""Entrena el predictor de devolucions (pipeline de 04-03 + regressió logística) i el desa.
Ús: python -m novamarket.entrenar --n 3000 --llavor 42 --sortida models/model_devolucions.joblib
"""
import argparse, json, platform
from pathlib import Path
import joblib, sklearn
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from novamarket.dades import (crear_preparacio, embrutar_comandes,
generar_comandes_ml, preparar_comandes)
def entrenar(n=3000, llavor=42):
"""Genera les dades, entrena i retorna (pipeline entrenat, AUC de test)."""
X, y = preparar_comandes(embrutar_comandes(generar_comandes_ml(n, llavor), llavor))
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.25, random_state=llavor, stratify=y)
pipe = Pipeline([("prep", crear_preparacio()),
("model", LogisticRegression(max_iter=1000))]).fit(Xtr, ytr)
auc = roc_auc_score(yte, pipe.predict_proba(Xte)[:, 1])
return pipe, auc
def desar(pipe, auc, ruta, n, llavor):
"""Desa el model i, al costat, un JSON amb mètrica, paràmetres i versions (reproduïbilitat)."""
ruta = Path(ruta)
ruta.parent.mkdir(parents=True, exist_ok=True)
joblib.dump(pipe, ruta)
meta = {"auc_test": round(float(auc), 4), "n": n, "llavor": llavor,
"python": platform.python_version(), "scikit_learn": sklearn.__version__}
ruta.with_suffix(".json").write_text(json.dumps(meta, indent=2))
return meta
if __name__ == "__main__": # només s'executa en llançar el fitxer com a script
parser = argparse.ArgumentParser(description="Entrena el predictor de devolucions de NovaMarket")
parser.add_argument("--n", type=int, default=3000)
parser.add_argument("--llavor", type=int, default=42)
parser.add_argument("--sortida", default="models/model_devolucions.joblib")
args = parser.parse_args()
pipe, auc = entrenar(args.n, args.llavor)
meta = desar(pipe, auc, args.sortida, args.n, args.llavor)
print(f"AUC test: {auc:.3f} -> {args.sortida}")
print(meta)if __name__ == "__main__": és la línia que fa possibles totes dues coses: quan executes el fitxer, __name__ val "__main__" i corre el bloc; quan un altre mòdul (o un test) fa from novamarket.entrenar import entrenar, __name__ val "novamarket.entrenar" i el bloc no s'executa. argparse converteix --n 3000 en args.n, amb valors per defecte i --help gratis.
5. pyproject.toml: el fitxer estàndard de metadades del projecte; aquí també li diu a pytest on és el codi:
[project]
name = "novamarket"
version = "0.1.0"
description = "Projecte d'IA de NovaMarket (curs Fonaments d'IA)"
requires-python = ">=3.10"
[build-system]
requires = ["setuptools>=64"]
build-backend = "setuptools.build_meta"
[tool.setuptools.packages.find]
where = ["src"]
[tool.pytest.ini_options]
pythonpath = ["src"]
testpaths = ["tests"]pip install -e . (instal·lació editable) registra el paquet a l'entorn apuntant a src/, de manera que import novamarket funciona des de qualsevol carpeta i els canvis al codi es veuen sense reinstal·lar. Alternativa sense instal·lar: PYTHONPATH=src python -m novamarket.entrenar.
6. tests/test_dades.py i tests/test_entrenar.py: pytest descobreix funcions test_* i comprova els assert. Provem el que no es pot trencar: reproduïbilitat, forma, les 21 columnes, absència de NaN, un AUC mínim i que el desament escriu els dos fitxers:
# tests/test_dades.py
import numpy as np
from novamarket.dades import generar_comandes_ml, embrutar_comandes, preparar_comandes, crear_preparacio
def test_generacio_reproduible():
a = generar_comandes_ml(500, llavor=42)
b = generar_comandes_ml(500, llavor=42)
assert a.equals(b) # mateixa llavor -> mateixes dades
assert not a.equals(generar_comandes_ml(500, llavor=7))
def test_forma_i_columnes():
comandes = generar_comandes_ml(3000, llavor=42)
assert comandes.shape == (3000, 7)
assert set(comandes.columns) >= {"import_comanda", "categoria", "retornat"}
assert set(comandes["retornat"].unique()) == {0, 1}
assert 0.10 < comandes["retornat"].mean() < 0.25 # taxa de devolució plausible (~16 %)
def test_preparacio_produeix_21_columnes():
X, y = preparar_comandes(embrutar_comandes(generar_comandes_ml(1000, 42), 42))
matriu = crear_preparacio().fit_transform(X)
assert matriu.shape == (len(X), 21) # les 21 columnes de 04-03
assert not np.isnan(matriu).any() # sense NaN després d'imputar
assert len(X) == len(y)# tests/test_entrenar.py
from novamarket.entrenar import entrenar, desar
def test_entrenar_auc_raonable(tmp_path): # tmp_path: carpeta temporal que pytest crea i esborra
pipe, auc = entrenar(n=1500, llavor=42)
assert auc > 0.75 # llindar de qualitat mínim
meta = desar(pipe, auc, tmp_path / "m.joblib", 1500, 42)
assert (tmp_path / "m.joblib").exists() and (tmp_path / "m.json").exists()
assert meta["auc_test"] == round(auc, 4)7. requirements.txt, .gitignore i README.md com a les seccions 3, 8 i 9 (el .gitignore ignora .venv/, __pycache__/, .ipynb_checkpoints/, data/raw/* i data/processed/* llevat dels .gitkeep, models/*.joblib, models/*.pt i .env).
8. Executar. Tests, entrenament i comprovació de Git, amb les sortides reals:
$ python -m pytest -v
tests/test_dades.py::test_generacio_reproduible PASSED [ 25%]
tests/test_dades.py::test_forma_i_columnes PASSED [ 50%]
tests/test_dades.py::test_preparacio_produeix_21_columnes PASSED [ 75%]
tests/test_entrenar.py::test_entrenar_auc_raonable PASSED [100%]
============================== 4 passed in 0.92s ===============================
$ pip install -e .
Successfully installed novamarket-0.1.0
$ python -m novamarket.entrenar --n 3000 --llavor 42
AUC test: 0.844 -> models/model_devolucions.joblib
{'auc_test': 0.8444, 'n': 3000, 'llavor': 42, 'python': '3.13.5', 'scikit_learn': '1.9.0'}
$ git init && git add -A && git status --short
A .gitignore
A README.md
A data/processed/.gitkeep
A data/raw/.gitkeep
A models/.gitkeep
A models/model_devolucions.json <- el JSON petit SÍ; el .joblib NO apareix (ignorat)
A pyproject.toml
A requirements.txt
A src/novamarket/__init__.py
A src/novamarket/dades.py
A src/novamarket/entrenar.py
A tests/test_dades.py
A tests/test_entrenar.pyQuatre tests en menys d'un segon, el mateix AUC 0,844 de 04-04 obtingut amb una ordre des del terminal, un JSON que diu amb quines versions es va entrenar, i un repositori en què el model binari no entra però les seves mètriques sí. Aquest és el punt de partida del mòdul 8: quan a 08-01 hi afegim predir.py (que carrega models/model_devolucions.joblib com a 07-03) o canviem el model, els tests diran en un segon si alguna cosa s'ha trencat.
- GPU, CPU i núvol: quan cal
Tot el curs ha corregut en CPU, i aquesta és la resposta per a la major part del que fa NovaMarket: taules de milers o centenars de milers de files amb scikit-learn, l'MLP de 497 paràmetres, les regles, l'optimització. La GPU cal quan el càlcul són grans multiplicacions de matrius repetides: entrenar CNN sobre imatges reals, afinar un transformer, executar LLM locals; aquí accelera de 10 a 100 vegades. Comprovar-la i fer-la servir a PyTorch:
import torch
print(torch.cuda.is_available()) # False a l'entorn del curs (PyTorch CPU)
dispositiu = torch.device("cuda" if torch.cuda.is_available() else "cpu")
xarxa = xarxa.to(dispositiu); xb = xb.to(dispositiu) # model i dades han de ser al mateix lloc(A Mac amb Apple Silicon l'equivalent és torch.backends.mps.is_available() i "mps".) Opcions, de menor a major cost: la CPU del portàtil; Colab/Kaggle per provar amb GPU gratis (secció 6); una màquina virtual amb GPU al núvol (AWS, Azure, Google Cloud i proveïdors especialitzats) que es paga per hora i s'apaga en acabar; serveis gestionats d'ML d'aquests proveïdors; o una GPU pròpia si l'ús és continu. La pregunta del Diego ("cal?") es respon mesurant: si l'entrenament en CPU triga minuts, no; si triga dies, sí, i tot i així convé primer reduir dades o model per iterar ràpid i fer servir la GPU només per a l'execució final.
- Docker com a entorn reproduïble
L'últim esglaó de la reproduïbilitat és empaquetar la màquina sencera: sistema operatiu, Python, llibreries amb versió, codi i ordre d'arrencada, en una imatge que corre igual al portàtil de la Marta, al servidor de NovaMarket i al núvol. Això és Docker (o Podman). Un Dockerfile il·lustratiu per servir el predictor amb l'API de 07-03:
# Il·lustratiu: imatge per servir el predictor de devolucions de NovaMarket
FROM python:3.12-slim # base: Linux mínim amb Python
WORKDIR /app # carpeta de treball dins del contenidor
COPY requirements.txt . # primer les dependències (es guarden a la memòria cau si no canvien)
RUN pip install --no-cache-dir -r requirements.txt fastapi uvicorn
COPY src/ src/ # el paquet novamarket
COPY models/model_devolucions.joblib models/
COPY servir.py . # l'API FastAPI de 07-03
ENV PYTHONPATH=/app/src
EXPOSE 8000
CMD ["uvicorn", "servir:app", "--host", "0.0.0.0", "--port", "8000"]docker build -t novamarket-devolucions . construeix la imatge i docker run -p 8000:8000 novamarket-devolucions l'arrenca; el web de NovaMarket crida http://servidor:8000/risc-devolucio. Docker no substitueix venv en el dia a dia del desenvolupament (és més pesant), però és la manera estàndard de lliurar un servei i de garantir que "a la meva màquina funciona" vulgui dir "a totes". Kubernetes (07-01) orquestra molts contenidors; fora de l'abast del curs.
Errors Comuns i Consells
- Instal·lar-ho tot al Python del sistema. Tard o d'hora dos projectes demanen versions incompatibles i alguna cosa del sistema deixa de funcionar. Un entorn per projecte, sempre.
requirements.txtsense versions, o senserequirements.txt. Sis mesos després,pip installporta versions noves i eljoblibno carrega o el número canvia. Fixa amb==el que importa i guarda unfreeze.- Notebooks que només funcionen en l'ordre en què es van executar. "Restart Kernel and Run All" abans de donar res per bo; funcions a mòduls; sortides netes abans de pujar a Git.
- Dades i models a Git. El repositori s'engreixa fins a ser inutilitzable i pots publicar dades personals.
.gitignoredes del primer commit; DVC o emmagatzematge extern per al que és gran. - Llavors sense versions o versions sense llavors. Totes dues, i anotades al costat del model (el JSON de la secció 10).
- Tests que proven el que és trivial i no el que és fràgil. Prova la forma de la matriu, l'absència de NaN, la reproduïbilitat, un llindar de mètrica; són les fallades que de debò passen en tocar
preparar_comandes. - Comprar GPU abans de mesurar. Cronometra en CPU, redueix el problema, prova a Colab; després decideix.
- Rutes absolutes al codi (
/home/marta/comandes.csv). Rutes relatives a l'arrel del projecte o configurables per argument;pathlib.Pathen lloc de concatenar cadenes.
Exercicis
Exercici 1. Afegeix al projecte un mòdul src/novamarket/predir.py amb una funció carregar(ruta="models/model_devolucions.joblib") i una altra predir_risc(model, comanda: dict) -> float que construeixi un DataFrame d'una fila i retorni la probabilitat de devolució, més un bloc if __name__ == "__main__": que predigui per a una comanda d'exemple. Escriu un test que entreni amb entrenar(n=1500), desi a tmp_path, carregui amb carregar i comprovi que predir_risc retorna un número entre 0 i 1 i igual a pipe.predict_proba sobre la mateixa comanda.
Exercici 2. Simula el problema de l'estat ocult: en un notebook (o mentalment, descrivint les cel·les), defineix a la cel·la 1 llindar = 0.5, a la cel·la 2 una funció que faci servir llindar, executa la cel·la 3 que la crida; després canvia la cel·la 1 a llindar = 0.3 sense reexecutar-la i torna a executar la 3. Quin llindar s'aplica? Què mostra el notebook? Què passa en fer "Restart and Run All"? Proposa dos canvis en l'organització del notebook que evitin el problema.
Exercici 3. Escriu el .gitignore, el requirements.txt (amb les versions que facis servir) i un Dockerfile il·lustratiu per a un projecte que serveixi la xarxa bayesiana de diagnòstic d'incidències de 06-03 com a API. Justifica què entra a Git i què no, i per què en aquest cas el "model" (les taules de probabilitat condicional) probablement sí que s'hauria de versionar a Git, a diferència de model_devolucions.joblib.
Solucions
Solució 1.
# src/novamarket/predir.py
"""Carrega el predictor de devolucions i prediu el risc d'una comanda."""
import joblib, pandas as pd
def carregar(ruta="models/model_devolucions.joblib"):
return joblib.load(ruta)
def predir_risc(model, comanda):
"""comanda: dict amb les 15 columnes de preparar_comandes. Retorna la probabilitat de devolució."""
X = pd.DataFrame([comanda])
return float(model.predict_proba(X)[0, 1])
if __name__ == "__main__":
model = carregar()
exemple = {"import_comanda": 72.99, "num_articles": 4, "dies_lliurament": 6.0, "client_nou": 0,
"categoria": "informatica", "codi_postal_zona": "A", "metode_pagament": "targeta",
"tipus_enviament": "estandard", "dia_setmana": 3, "mes": 11, "cap_de_setmana": 0,
"dies_des_inici": 87, "import_per_article": 18.25, "comandes_previes": 3,
"taxa_devolucio_previa": 0.0}
print(f"risc de devolució: {predir_risc(model, exemple):.3f}")# tests/test_predir.py
from novamarket.entrenar import entrenar, desar
from novamarket.predir import carregar, predir_risc
from novamarket.dades import generar_comandes_ml, embrutar_comandes, preparar_comandes
def test_predir_coincideix_amb_pipeline(tmp_path):
pipe, auc = entrenar(n=1500, llavor=42)
desar(pipe, auc, tmp_path / "m.joblib", 1500, 42)
model = carregar(tmp_path / "m.joblib")
X, _ = preparar_comandes(embrutar_comandes(generar_comandes_ml(500, 3), 3))
comanda = X.iloc[0].to_dict()
p = predir_risc(model, comanda)
assert 0.0 <= p <= 1.0
assert abs(p - pipe.predict_proba(X.iloc[[0]])[0, 1]) < 1e-9Executat a l'entorn del curs: pytest passa 5 tests, i python -m novamarket.predir imprimeix risc de devolució: 0.048 (la mateixa comanda de 07-03). Amb això el projecte té les tres peces del cicle (dades, entrenar, predir) provades.
Solució 2. S'aplica 0,5: la cel·la 1 es va editar però no es va executar, així que la variable llindar del kernel continua valent 0,5; el notebook mostra llindar = 0.3 a la cel·la 1 i un resultat calculat amb 0,5, és a dir, menteix. En fer "Restart and Run All" el kernel es buida, s'executa la cel·la 1 amb 0,3 i el resultat canvia; si algú hagués copiat el resultat anterior a un informe, seria irreproduïble. Dos canvis: (1) paràmetres i funcions al principi, i la regla de reexecutar des de dalt després de qualsevol canvi de paràmetre (o directament "Run All Above" abans de la cel·la de resultats); (2) treure la funció a src/novamarket/ amb llindar com a argument explícit (decidir(prob, llindar=0.5)) i cridar-la amb el valor visible a la cel·la, de manera que no hi hagi cap variable global de la qual dependre.
Solució 3. .gitignore: .venv/, __pycache__/, .ipynb_checkpoints/, .env, i data/raw/* (els històrics d'incidències reals, confidencials); no s'ignora src/novamarket/xarxa_incidencies.py ni un configs/cpt_incidencies.json amb les taules de probabilitat. requirements.txt: numpy==..., pandas==..., fastapi, uvicorn, pytest (i pgmpy==... si se substitueix l'enumeració de 06-03 per pgmpy, 07-03). Dockerfile: igual que el de la secció 12, copiant src/, configs/ i servir_incidencies.py, amb CMD ["uvicorn", "servir_incidencies:app", ...]. Per què el "model" sí que va a Git: les taules de probabilitat condicional de la xarxa de 06-03 són poques desenes de números escrits o revisats per persones (coneixement explícit, com les regles de 06-02), petites, llegibles com a text i amb valor d'auditoria (qui va canviar la probabilitat de "defecte de fàbrica" i quan?): exactament el que Git fa bé. model_devolucions.joblib és un binari generat per codi a partir de dades: es regenera amb entrenar.py, no es llegeix ni es revisa línia a línia, i el seu lloc és un magatzem d'artefactes amb el JSON de mètriques i versions a Git.
Conclusió
Amb aquesta lliçó el taller de la Marta queda muntat. Hem vist per què "a la meva màquina funciona" és l'enemic i com es combat: entorns virtuals (venv) amb un intèrpret i uns paquets per projecte; dependències fixades a requirements.txt (o environment.yml, uv, poetry) i l'equació de la reproduïbilitat, versions + llavors + codi + dades identificades; Jupyter per explorar i explicar, amb la disciplina de reiniciar i executar-ho tot i de moure el codi madur a mòduls; Colab i Kaggle com a laboratoris amb GPU gratuïta i sessions efímeres; VS Code i PyCharm amb depurador i l'intèrpret de l'entorn; Git amb la frontera clara entre el que es versiona (codi, dependències, mètriques petites) i el que no (dades, models grans, secrets), i DVC a l'horitzó; una estructura de projecte (data/, notebooks/, src/novamarket/, models/, tests/) que hem construït de debò, amb dades.py, entrenar.py executable des del terminal (AUC 0,844 i un JSON amb versions), un pyproject.toml i quatre tests de pytest que passen en un segon; i les decisions d'infraestructura: CPU llevat de prova del contrari, GPU i núvol quan el càlcul ho demani, Docker per lliurar el servei igual a tot arreu.
I amb això tanquem el mòdul 7. Vam començar a 07-01 triant Python i SQL entre els llenguatges de la IA; a 07-02 vam aprendre a manejar de debò NumPy, pandas i Matplotlib amb les comandes i la demanda de NovaMarket; a 07-03 vam situar cada llibreria a la seva tasca i vam desar i recarregar el predictor de devolucions i l'MLP; i a 07-04 ho hem ordenat tot en un projecte reproduïble amb entorn, estructura, Git i tests. La Marta té el llenguatge, les eines i el taller; el Diego té un entrenar.py que qualsevol pot executar i un repositori que no s'engreixa amb dades. El que encara no tenen és un mètode per portar un cas d'ús de la idea a producció: definir el problema de negoci, decidir la mètrica, muntar les dades, iterar sobre el model, validar-lo, desplegar-lo i vigilar-lo, aprenent dels projectes que han sortit bé i dels que no. És el mòdul 8, Projectes i Casos d'Estudi, que comença a 08-01, Desenvolupament d'un Projecte d'IA, amb la Marta i el Diego abordant de cap a cap, dins de l'estructura novamarket_ia/ que acabem de crear, el projecte del predictor de devolucions.
Fonaments d'Intel·ligència Artificial (IA)
Mòdul 1: Introducció a la Intel·ligència Artificial
Mòdul 2: Principis Bàsics de la IA
- Conceptes Fonamentals: Agents, Entorns i Racionalitat
- Tipus d'Intel·ligència Artificial
- Les Dades com a Matèria Primera de la IA
- Ètica i Consideracions en IA
Mòdul 3: Algorismes en IA
- Introducció als Algorismes
- Algorismes de Cerca
- Cerca amb Adversari: Jocs i Minimax
- Algorismes d'Optimització
Mòdul 4: Aprenentatge Automàtic (Machine Learning)
- Conceptes Bàsics de Machine Learning
- Tipus d'Aprenentatge Automàtic
- Preparació de Dades i Característiques
- Algorismes de Machine Learning
- Avaluació i Validació de Models
- Sobreajust, Regularització i Ajust d'Hiperparàmetres
Mòdul 5: Xarxes Neuronals i Deep Learning
- Introducció a les Xarxes Neuronals
- Arquitectura de Xarxes Neuronals
- Com Aprèn una Xarxa: Descens del Gradient i Retropropagació
- Deep Learning i les seves Aplicacions
- Transformers, Grans Models de Llenguatge i IA Generativa
Mòdul 6: Lògica i Sistemes Experts
- Lògica en IA
- Sistemes Experts
- Raonament amb Incertesa: Probabilitat i Xarxes Bayesianes
- Aplicacions dels Sistemes Experts
Mòdul 7: Eines i Llenguatges de Programació en IA
- Llenguatges de Programació per a IA
- Python Científic: NumPy, pandas i Matplotlib
- Eines i Llibreries Populars
- Entorns de Desenvolupament
Mòdul 8: Projectes i Casos d'Estudi
Mòdul 9: Exercicis i Pràctiques
- Exercicis d'Algorismes
- Pràctiques de Machine Learning
- Projectes de Xarxes Neuronals
- Projecte Integrador: de la Idea al Prototip
