Python per a Intel·ligència Artificial
Introducció
Python no va nixer per a la intel·ligència artificial. Es va crear el 1991 per Guido van Rossum com un llenguatge de scripting llegible i accessible. Pero la combinacio d'una sintaxi clara, una comunitat enorme i l'emergencia de biblioteques especialitzades el van convertir, de manera quasi accidental, en el llengua franca de la IA moderna.
Entendre Python per a IA no es simplement coneixer la sintaxi basica: es comprendre l'ecosistema sencer, saber triar les eines adequades per a cada problema, escriure codi eficient i mantenible, i desplegar aplicacions en entorns de produccio reals. Aixo es el que cobreix aquest tema.
1. Per que Python domina la IA?
1.1 Historia i context
El 2012, quan AlexNet va guanyar ImageNet i va marcar l'inici del renaixement del deep learning, la majoria del codi de la comunitat de recerca ja estava en Python. Theano, la biblioteca que va precedir TensorFlow i PyTorch, era Python. Caffe (2014) tenia bindings de Python. Quan Google va publicar TensorFlow el 2015 i Facebook PyTorch el 2016, ambdos van triar Python com a interficie principal.
La decisio no va ser arbitraria. Python oferia:
- Interactivitat: el REPL de Python i, especialment, els Jupyter Notebooks permeten explorar dades i models de manera iterativa, imprescindible en la investigacio.
- Ecosistema cientific preexistent: NumPy, SciPy i Matplotlib existien des de principis dels 2000 i la comunitat cientifica ja els usava.
- Facilitat d'aprenentatge: els investigadors d'IA provenen de matematiques, fisica o estadistica, no necessariament de la informatica. Python es mes accessible que Java o C++.
- Velocitat de prototipat: una idea es pot implementar i provar en hores, no en dies.
1.2 El Global Interpreter Lock (GIL) i per que no importa tant
Una critica habitual a Python es el GIL (Global Interpreter Lock): un mutex que impedeix que multiples fils d'execucio de Python corregin codi Python pur en paral·lel al mateix temps. En teoria, aixo hauria de fer Python inadequat per a tasques computacionalment intensives com l'entrenament de models.
A la practica, el GIL no es un problema per a la IA perque:
- Les operacions costoses no son en Python pur: NumPy, PyTorch i TensorFlow delegen el calcul a biblioteques C/C++/CUDA que alliberen el GIL durant la seva execucio. El paral·lelisme real succeeix a nivell de C/CUDA.
- GPU computing: l'entrenament de deep learning es fa principalment a la GPU, que te el seu propi model de paral·lelisme massiu (CUDA threads). El GIL no hi te cap efecte.
- Multiprocessing: per a tasques CPU-bound que necessiten paral·lelisme real, Python ofereix el modul
multiprocessingque usa processos separats (cada un amb el seu GIL).
Python 3.13 i el free-threaded mode
Python 3.13 (octubre 2024) introdueix en fase experimental la possibilitat de desactivar el GIL (python3.13t). Aixo obre la porta a un veritable paral·lelisme de fils en Python pur. Es massa recent per a ser estandard en produccio, pero es una tendencia important a seguir.
1.3 Alternatives reals: quan triar Rust, Julia o C++?
Rust es la millor opcio quan necessites: - Rendiment maxim amb seguretat de memoria garantida - Eines de CLI o serveis de baix nivell - Processament d'inferencia sense el overhead de Python
Exemples reals: candle (Hugging Face, motors d'inferencia en Rust), polars (DataFrame ultrarapid), tokenizers (tokenitzador de HF en Rust cridat des de Python).
Julia es superior quan: - Treballes en simulacio numerica o calcul diferencial - Necesites velocitat C amb sintaxi Python-like - L'ecosistema de Julia cobreix el teu domini (astrofisica, bioinformatica, economia quantitativa)
C++ es necessari quan: - Implementes kernels de GPU (CUDA) - Construeixes biblioteques que seran usades des de Python (via pybind11 o ctypes) - Treballes en sistemes encastats o en temps real
Recomanacio practica
Per a aquest curs i per a la gran majoria de projectes professionals d'IA el 2025, Python es l'eleccio correcta. Les alternatives son complementaries, no substitutes.
2. Entorns i gestio de dependencies
Un dels problemes mes comuns en projectes Python es la gestio de dependencies: que passa quan dos projectes necessiten versions incompatibles d'una mateixa biblioteca? La solucio son els entorns virtuals.
2.1 venv: el mes senzill
venv es el modul estandard de Python per crear entorns virtuals aïllats:
# Crear un entorn virtual
python -m venv entorn-ia
# Activar-lo (Linux/Mac)
source entorn-ia/bin/activate
# Activar-lo (Windows PowerShell)
.\entorn-ia\Scripts\Activate.ps1
# Instal·lar dependencies
pip install langchain openai chromadb
# Guardar les dependencies
pip freeze > requirements.txt
# Desactivar l'entorn
deactivate
Limitacio: venv no gestiona la versio de Python en si, nomes les biblioteques.
2.2 conda: gestor complet
conda (part d'Anaconda o Miniconda) gestiona tant les biblioteques Python com la versio de Python i fins i tot biblioteques no-Python (CUDA, MKL, etc.):
# Crear entorn amb versio especifica de Python
conda create -n iabd-5073 python=3.11
# Activar
conda activate iabd-5073
# Instal·lar (conda-forge te moltes mes biblioteques que pip)
conda install -c conda-forge numpy pandas scikit-learn
pip install langchain openai # per a les que nomes estan a PyPI
# Exportar entorn complet (inclou versio Python)
conda env export > environment.yml
# Recrear entorn des de fitxer
conda env create -f environment.yml
2.3 Poetry: gestio moderna de dependencies
Poetry es l'eina recomanada per a projectes Python professionals. Gestiona dependencies, resolucio de conflictes, publicacio a PyPI i entorns virtuals de manera integrada:
# Instal·lar Poetry
curl -sSL https://install.python-poetry.org | python3 -
# Crear nou projecte
poetry new el-meu-projecte-ia
cd el-meu-projecte-ia
# Afegir dependencies
poetry add langchain openai chromadb fastapi
# Afegir dependencies de desenvolupament
poetry add --group dev pytest ruff mypy
# Instal·lar totes les dependencies
poetry install
# Executar codi dins l'entorn de Poetry
poetry run python main.py
# Activar el shell de Poetry
poetry shell
El fitxer pyproject.toml que genera Poetry especifica les dependencies de manera declarativa:
[tool.poetry]
name = "assistant-rag"
version = "0.1.0"
description = "Assistent RAG en catala"
authors = ["Joan Garcia <joan@sapalomera.cat>"]
[tool.poetry.dependencies]
python = "^3.11"
langchain = "^0.3.0"
openai = "^1.0.0"
chromadb = "^0.5.0"
fastapi = "^0.115.0"
[tool.poetry.group.dev.dependencies]
pytest = "^8.0"
ruff = "^0.6"
mypy = "^1.10"
2.4 Docker: la solucio definitiva per a reproducibilitat
Per a les practiques d'aquest curs, usem Docker com a entorn primari. Docker garanteix que el codi funcioni de manera identica a tots els ordinadors, independentment del sistema operatiu.
# Dockerfile base per a practiques de IA
FROM python:3.11-slim
# Variables d'entorn per a Python
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1
# Directori de treball
WORKDIR /app
# Instal·lar dependencies del sistema
RUN apt-get update && apt-get install -y \
build-essential \
curl \
&& rm -rf /var/lib/apt/lists/*
# Copiar requirements i instal·lar
COPY requirements.txt .
RUN pip install --upgrade pip && \
pip install -r requirements.txt
# Copiar codi de l'aplicacio
COPY . .
# Port per defecte
EXPOSE 8000
# Healthcheck
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1
# Comando per defecte
CMD ["python", "main.py"]
# docker-compose.yml per a practiques del modul
version: "3.8"
services:
app-ia:
build: .
container_name: app-ia-joan-garcia
environment:
- OPENAI_API_KEY=${OPENAI_API_KEY}
- ALUMNE=joan-garcia
volumes:
- ./app:/app
- ./data:/data/joan-garcia
ports:
- "8000:8000"
restart: unless-stopped
Miniactivitat
Crea un Dockerfile per a un entorn Python 3.11 que inclogui numpy, pandas i scikit-learn. Construeix-lo amb docker build -t entorn-ia . i verifica que pots executar python -c "import sklearn; print(sklearn.__version__)" dins del contenidor.
3. Jupyter Notebooks vs scripts vs aplicacions
La tria entre Jupyter Notebooks, scripts Python i aplicacions completes no es arbitraria: cada format te el seu lloc en el cicle de vida d'un projecte d'IA.
3.1 Jupyter Notebooks: per a exploracio i ensenyament
Els notebooks son ideals per a: - Analisi exploratoria de dades (EDA): veure distribucions, detectar outliers, explorar correlacions - Experimentacio amb models: provar hyperparametres, comparar algorismes - Ensenyament i documentacio: barrejar explicacio i codi en el mateix document - Comunicacio de resultats: crear informes executables
# Exemple tipic de notebook per a EDA
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
# Cargar dades
df = pd.read_csv('dataset_clients.csv')
# Informacio basica
print(df.info())
print(df.describe())
# Visualitzar distribucio d'edat
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
df['edat'].hist(ax=axes[0], bins=30, color='steelblue', edgecolor='white')
axes[0].set_title('Distribucio d\'edat')
sns.boxplot(data=df, x='segment', y='edat', ax=axes[1])
axes[1].set_title('Edat per segment de client')
plt.tight_layout()
plt.savefig('eda_clients.png', dpi=150)
plt.show()
Limitacions dels notebooks: - Dificil de versionar amb Git (format JSON amb outputs) - No apte per a produccio directament - El estat global (cells executades en ordre no lineal) pot causar bugs subtils - Dificil de testejar de manera automatica
3.2 Scripts Python: per a tasques automatitzades
Un script es un fitxer .py que executa una tasca especifica de manera automatitzada:
#!/usr/bin/env python3
"""
Script d'inferencia batch: classifica un CSV de textos amb un model de sentiment.
Us: python inferencia_batch.py --input dades.csv --output resultats.csv
"""
import argparse
import logging
import pandas as pd
from transformers import pipeline
# Configurar logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
def main(input_path: str, output_path: str, batch_size: int = 32) -> None:
logger.info(f"Carregant dades de {input_path}")
df = pd.read_csv(input_path)
logger.info("Inicialitzant model de sentiment...")
classifier = pipeline(
"sentiment-analysis",
model="cardiffnlp/twitter-xlm-roberta-base-sentiment-multilingual",
device=-1 # CPU; usar 0 per a GPU
)
textos = df['text'].tolist()
logger.info(f"Processant {len(textos)} textos en batches de {batch_size}...")
resultats = classifier(textos, batch_size=batch_size, truncation=True)
df['sentiment'] = [r['label'] for r in resultats]
df['confianca'] = [round(r['score'], 4) for r in resultats]
df.to_csv(output_path, index=False)
logger.info(f"Resultats guardats a {output_path}")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Inferencia de sentiment en batch")
parser.add_argument("--input", required=True, help="Fitxer CSV d'entrada")
parser.add_argument("--output", required=True, help="Fitxer CSV de sortida")
parser.add_argument("--batch-size", type=int, default=32, help="Mida del batch")
args = parser.parse_args()
main(args.input, args.output, args.batch_size)
3.3 Aplicacions: per a produccio
Una aplicacio d'IA en produccio te una estructura mes completa, amb moduls separats, testing, logging estructurat i API:
app-ia/
├── pyproject.toml # dependencies (Poetry)
├── Dockerfile
├── docker-compose.yml
├── README.md
├── src/
│ ├── __init__.py
│ ├── main.py # punt d'entrada FastAPI
│ ├── models/
│ │ ├── __init__.py
│ │ ├── llm.py # client LLM
│ │ └── embeddings.py # model d'embeddings
│ ├── services/
│ │ ├── __init__.py
│ │ ├── rag.py # logica RAG
│ │ └── chat.py # logica de chat
│ └── schemas/
│ ├── __init__.py
│ └── requests.py # Pydantic models
├── tests/
│ ├── __init__.py
│ ├── test_rag.py
│ └── test_api.py
└── data/
└── documents/ # documents per al RAG
4. Biblioteques essencials per a IA
4.1 NumPy: la base de tot
NumPy (Numerical Python) es la biblioteca de calcul numeric fonamental. Quasi totes les altres biblioteques d'IA (PyTorch, scikit-learn, etc.) usen arrays de NumPy com a estructura de dades base.
import numpy as np
# Arrays: mes eficients que llistes Python per a calcul
a = np.array([1, 2, 3, 4, 5], dtype=np.float32)
b = np.array([10, 20, 30, 40, 50], dtype=np.float32)
# Operacions vectoritzades (sense bucle Python)
suma = a + b # [11, 22, 33, 44, 55]
producte = a * b # element-wise: [10, 40, 90, 160, 250]
producte_escalar = np.dot(a, b) # 550.0
# Broadcasting: operacions entre arrays de dimensions compatibles
matriu = np.random.randn(100, 768) # 100 embeddings de dimensio 768
normalitzat = (matriu - matriu.mean(axis=0)) / matriu.std(axis=0)
# Indexacio avancada
indices_positius = matriu[:, 0] > 0 # masquara booleana
subset = matriu[indices_positius] # files on la primera columna es positiva
# Calcul de similitud cosinus entre dos embeddings
def similitud_cosinus(v1: np.ndarray, v2: np.ndarray) -> float:
"""Calcula la similitud cosinus entre dos vectors."""
return np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2))
emb1 = np.random.randn(768)
emb2 = np.random.randn(768)
sim = similitud_cosinus(emb1, emb2)
print(f"Similitud cosinus: {sim:.4f}")
4.2 Pandas: analisi i manipulacio de dades
Pandas es la biblioteca estandard per a analisi de dades tabulars. El 2025, Pandas 2.x incorpora el backend PyArrow que el fa significativament mes rapid:
import pandas as pd
# Carregar dades reals
df = pd.read_csv('dataset_ia_benchmarks.csv')
# Operacions basiques
print(df.shape) # (files, columnes)
print(df.dtypes) # tipus de cada columna
print(df.isnull().sum()) # valors nuls per columna
# Filtrar i seleccionar
models_grans = df[df['parametres_billions'] > 70]
top_models = df.nlargest(10, 'mmlu_score')[['model', 'mmlu_score', 'cost_per_1m_tokens']]
# Agregacio
per_empresa = df.groupby('empresa').agg({
'mmlu_score': ['mean', 'max'],
'cost_per_1m_tokens': 'min'
}).round(2)
# Pipelines de transformacio
df_net = (df
.dropna(subset=['mmlu_score'])
.assign(
cost_category=pd.cut(df['cost_per_1m_tokens'], bins=[0, 1, 10, 100],
labels=['baix', 'mitja', 'alt'])
)
.query("parametres_billions >= 7")
.sort_values('mmlu_score', ascending=False)
)
# Exportar
df_net.to_csv('models_filtrats.csv', index=False)
df_net.to_parquet('models_filtrats.parquet') # format binari eficient
4.3 scikit-learn: Machine Learning classic
scikit-learn ofereix una API consistent per a tots els algorismes de ML. El concepte central es l'estimador: qualsevol objecte amb metodes fit(), predict() i transform().
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler, LabelEncoder
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.model_selection import train_test_split, cross_val_score, GridSearchCV
from sklearn.metrics import classification_report, confusion_matrix
import pandas as pd
import numpy as np
# Exemple: classificacio de tickets de suport
df = pd.read_csv('tickets_suport.csv') # columnes: 'text', 'categoria'
X = df['text']
y = df['categoria']
# Divisio train/test
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
# Pipeline complet
pipeline = Pipeline([
('tfidf', TfidfVectorizer(max_features=10000, ngram_range=(1, 2))),
('scaler', StandardScaler(with_mean=False)), # no centra (TF-IDF es esparsa)
('clf', GradientBoostingClassifier(n_estimators=200, max_depth=5))
])
# Entrenar
pipeline.fit(X_train, y_train)
# Avaluar
y_pred = pipeline.predict(X_test)
print(classification_report(y_test, y_pred))
# Cross-validation
scores = cross_val_score(pipeline, X_train, y_train, cv=5, scoring='f1_weighted')
print(f"F1 CV: {scores.mean():.3f} ± {scores.std():.3f}")
# Cerca d'hyperparametres
param_grid = {
'tfidf__max_features': [5000, 10000],
'clf__n_estimators': [100, 200],
'clf__max_depth': [3, 5]
}
grid_search = GridSearchCV(pipeline, param_grid, cv=3, n_jobs=-1, verbose=2)
grid_search.fit(X_train, y_train)
print(f"Millors parametres: {grid_search.best_params_}")
4.4 PyTorch: deep learning modern
PyTorch es la biblioteca de deep learning preferida per la comunitat de recerca i cada cop mes en produccio. La seva filosofia "Pythonic" la fa mes intuïtiva que TensorFlow per a la majoria d'usos:
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset
# Verificar si hi ha GPU disponible
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Usant dispositiu: {device}")
# Xarxa neuronal senzilla per a classificacio binaria
class ClassificadorIA(nn.Module):
def __init__(self, input_dim: int, hidden_dim: int, num_classes: int):
super().__init__()
self.xarxa = nn.Sequential(
nn.Linear(input_dim, hidden_dim),
nn.BatchNorm1d(hidden_dim),
nn.ReLU(),
nn.Dropout(0.3),
nn.Linear(hidden_dim, hidden_dim // 2),
nn.ReLU(),
nn.Dropout(0.2),
nn.Linear(hidden_dim // 2, num_classes)
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.xarxa(x)
# Crear model i moure a GPU si disponible
model = ClassificadorIA(input_dim=768, hidden_dim=256, num_classes=5).to(device)
# Optimitzador i funcio de perdua
optimizer = optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4)
criterion = nn.CrossEntropyLoss()
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=10)
# Bucle d'entrenament
def entrenar_epoch(model, loader, optimizer, criterion, device):
model.train()
total_loss = 0
correctes = 0
for batch_x, batch_y in loader:
batch_x, batch_y = batch_x.to(device), batch_y.to(device)
optimizer.zero_grad()
sortida = model(batch_x)
loss = criterion(sortida, batch_y)
loss.backward()
# Gradient clipping per estabilitat
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
total_loss += loss.item()
correctes += (sortida.argmax(dim=1) == batch_y).sum().item()
return total_loss / len(loader), correctes / len(loader.dataset)
4.5 Hugging Face: l'ecosistema de models preentrenats
Hugging Face ha creat l'ecosistema obert mes important per a models de deep learning. El Hub allotja mes de 500.000 models (febrer 2025), i la biblioteca transformers permet usar-los amb una API unificada:
from transformers import pipeline, AutoTokenizer, AutoModelForSequenceClassification
import torch
# La manera mes simple: pipeline
# Detecta automaticament el model adequat per a la tasca
classificador = pipeline(
"text-classification",
model="pysentimiento/robertuito-sentiment-analysis", # model en espanyol/catala
device=0 if torch.cuda.is_available() else -1
)
textos = [
"L'assistencia al client ha estat excel·lent, estic molt satisfet.",
"El producte es terrible, no el recomano a ningu.",
"Es un producte normal, ni bo ni dolent."
]
resultats = classificador(textos, batch_size=4)
for text, resultat in zip(textos, resultats):
print(f"Text: {text[:50]}...")
print(f" Sentiment: {resultat['label']} (confianca: {resultat['score']:.3f})")
# Control mes fi: tokenitzador + model separat
tokenizer = AutoTokenizer.from_pretrained("sentence-transformers/paraphrase-multilingual-mpnet-base-v2")
model = AutoModelForSequenceClassification.from_pretrained(
"sentence-transformers/paraphrase-multilingual-mpnet-base-v2"
)
# Generar embeddings per a RAG
from sentence_transformers import SentenceTransformer
embed_model = SentenceTransformer("sentence-transformers/paraphrase-multilingual-mpnet-base-v2")
documents = [
"Python es el llenguatge principal per a IA.",
"LangChain facilita la creacio d'aplicacions amb LLMs.",
"Docker garanteix la reproducibilitat dels entorns."
]
embeddings = embed_model.encode(documents, normalize_embeddings=True)
print(f"Shape dels embeddings: {embeddings.shape}") # (3, 768)
Miniactivitat
Usa la biblioteca transformers de Hugging Face per carregar el pipeline de zero-shot-classification i classificar 5 textos en catala en categories que tu defineixis (per exemple: "tecnologia", "esport", "politica"). Executa-ho en un contenidor Docker amb python:3.11-slim.
4.6 LangChain i LangGraph: el marc per a aplicacions LLM
LangChain (versio 0.3+) organitza les aplicacions basades en LLMs al voltant de cadenes (chains) i agents. LangGraph afegeix el concepte de graf d'estat per a workflows mes complexos.
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_community.vectorstores import Chroma
from langchain_core.runnables import RunnablePassthrough
# LLM
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
# Cadena senzilla amb LCEL (LangChain Expression Language)
prompt = ChatPromptTemplate.from_messages([
("system", "Ets un assistent expert en IA. Respon sempre en catala."),
("human", "{pregunta}")
])
cadena = prompt | llm | StrOutputParser()
resposta = cadena.invoke({"pregunta": "Que es un transformer en el context de la IA?"})
print(resposta)
# Cadena RAG completa
def crear_cadena_rag(documents: list[str]) -> any:
"""Crea una cadena RAG amb els documents proporcionats."""
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
# Crear vector store
vectorstore = Chroma.from_texts(
texts=documents,
embedding=embeddings,
collection_name="modul-5073"
)
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
# Prompt RAG
prompt_rag = ChatPromptTemplate.from_messages([
("system", """Ets un assistent expert. Usa NOMES el context proporcionat per respondre.
Si no trobes la resposta al context, digues-ho clarament.
Context:
{context}"""),
("human", "{pregunta}")
])
# Cadena completa
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
cadena_rag = (
{"context": retriever | format_docs, "pregunta": RunnablePassthrough()}
| prompt_rag
| llm
| StrOutputParser()
)
return cadena_rag
4.7 FastAPI: desplegament d'APIs per a IA
FastAPI es el framework preferit per a desplegar models d'IA com a APIs REST. Generat automaticament documentacio Swagger i valida les dades d'entrada amb Pydantic:
from fastapi import FastAPI, HTTPException, BackgroundTasks
from pydantic import BaseModel, Field
from typing import Optional
import uvicorn
import logging
logger = logging.getLogger(__name__)
app = FastAPI(
title="API d'IA — Modul 5073",
description="API per a inferencia de sentiment i generacio de text",
version="1.0.0"
)
class PeticioSentiment(BaseModel):
text: str = Field(..., min_length=1, max_length=5000, description="Text a analitzar")
idioma: str = Field(default="ca", description="Idioma del text (ca, es, en)")
class RespostaSentiment(BaseModel):
text: str
sentiment: str
confianca: float
idioma: str
@app.get("/health")
async def health_check():
"""Endpoint de salut per a Docker healthcheck."""
return {"status": "ok", "versio": "1.0.0"}
@app.post("/analitzar-sentiment", response_model=RespostaSentiment)
async def analitzar_sentiment(peticio: PeticioSentiment):
"""Analitza el sentiment d'un text."""
try:
# Aqui aniria la crida al model real
# resultat = model.predict(peticio.text)
resultat = {"sentiment": "positiu", "confianca": 0.95}
return RespostaSentiment(
text=peticio.text,
sentiment=resultat["sentiment"],
confianca=resultat["confianca"],
idioma=peticio.idioma
)
except Exception as e:
logger.error(f"Error en analisi de sentiment: {e}")
raise HTTPException(status_code=500, detail=str(e))
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
5. Optimitzacio de Python per a IA
5.1 Vectoritzacio: evitar bucles Python
La regla d'or de Python per a IA: mai usar bucles Python per a operacions numeriques que NumPy/PyTorch poden fer. La vectoritzacio delega el calcul a codi C optimitzat.
import numpy as np
import time
n = 1_000_000
# MAL: bucle Python (molt lent)
def similituds_bucle(queries, documents):
resultats = []
for q in queries:
for d in documents:
sim = sum(a*b for a, b in zip(q, d)) # dot product
resultats.append(sim)
return resultats
# BE: operacio matricial vectoritzada
def similituds_vectoritzada(queries: np.ndarray, documents: np.ndarray) -> np.ndarray:
# Normalitzar
queries_norm = queries / np.linalg.norm(queries, axis=1, keepdims=True)
docs_norm = documents / np.linalg.norm(documents, axis=1, keepdims=True)
# Multiplicacio matricial: (q, d) x (d, docs) = (q, docs)
return queries_norm @ docs_norm.T
# Comparativa de temps
queries = np.random.randn(100, 768).astype(np.float32)
documents = np.random.randn(1000, 768).astype(np.float32)
t0 = time.perf_counter()
result = similituds_vectoritzada(queries, documents)
t1 = time.perf_counter()
print(f"Vectoritzada: {(t1-t0)*1000:.2f}ms per a {100*1000} similituds")
# Tipicament 10-100x mes rapid que el bucle Python
5.2 Async per a cridades a APIs
Quan fem multiples cridades a APIs d'IA (OpenAI, Anthropic...), usar asyncio permet fer-les en paral·lel i reduir el temps total dramaticament:
import asyncio
import time
from openai import AsyncOpenAI
client = AsyncOpenAI()
async def analitzar_text(text: str, semaphore: asyncio.Semaphore) -> dict:
"""Analitza un text amb GPT-4o-mini de manera asincrona."""
async with semaphore: # limitar a 10 cridades concurrents
resposta = await client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "Classifica el sentiment: positiu, negatiu, neutral."},
{"role": "user", "content": text}
],
max_tokens=10
)
return {"text": text, "sentiment": resposta.choices[0].message.content.strip()}
async def processar_batch(textos: list[str], max_concurrent: int = 10) -> list[dict]:
"""Processa un batch de textos en paral·lel."""
semaphore = asyncio.Semaphore(max_concurrent)
tasques = [analitzar_text(text, semaphore) for text in textos]
return await asyncio.gather(*tasques)
# Us
textos = ["Text 1...", "Text 2...", "Text 3..."] * 20 # 60 textos
t0 = time.perf_counter()
resultats = asyncio.run(processar_batch(textos))
t1 = time.perf_counter()
print(f"60 textos processats en {t1-t0:.2f}s (vs ~60s en serie)")
5.3 Profiling: trobar els colls d'ampolla
# cProfile: identificar quines funcions consumen mes temps
import cProfile
import pstats
import io
profiler = cProfile.Profile()
profiler.enable()
# Codi a perfilar
resultats = processar_documents(documents_llista)
profiler.disable()
# Mostrar resultats ordenats per temps
s = io.StringIO()
stats = pstats.Stats(profiler, stream=s).sort_stats('cumulative')
stats.print_stats(20) # top 20 funcions
print(s.getvalue())
6. Bones pràctiques per a codi d'IA
6.1 Tipus hints i Pydantic
El codi d'IA sol treballar amb estructures de dades complexes. Usar tipus hints i Pydantic evita errors subtils i millora l'autocomplete de l'IDE:
from pydantic import BaseModel, Field, validator
from typing import Optional, Literal
from datetime import datetime
class DocumentChunk(BaseModel):
"""Fragment d'un document per al vector store."""
id: str
text: str = Field(..., min_length=10)
font: str
pagina: Optional[int] = None
creacio: datetime = Field(default_factory=datetime.now)
@validator('text')
def text_no_buit(cls, v):
if v.strip() == "":
raise ValueError("El text no pot estar buit o ser nomes espais")
return v.strip()
class RespostaLLM(BaseModel):
pregunta: str
resposta: str
model_usat: str
tokens_entrada: int
tokens_sortida: int
cost_estimat_usd: float
fonts: list[str] = Field(default_factory=list)
confianca: Literal["alta", "mitjana", "baixa"] = "mitjana"
# Els tipus hints activen el type checker (mypy, pyright)
def cercar_i_respondre(
pregunta: str,
vectorstore: any,
llm: any,
top_k: int = 3
) -> RespostaLLM:
...
6.2 Logging estructurat
import logging
import json
from datetime import datetime
class JSONFormatter(logging.Formatter):
"""Formata els logs com a JSON per a ingesta en sistemes de monitoratge."""
def format(self, record: logging.LogRecord) -> str:
log_data = {
"timestamp": datetime.utcnow().isoformat(),
"level": record.levelname,
"message": record.getMessage(),
"module": record.module,
"function": record.funcName,
}
if record.exc_info:
log_data["exception"] = self.formatException(record.exc_info)
return json.dumps(log_data, ensure_ascii=False)
# Configurar logging
logger = logging.getLogger("app-ia")
handler = logging.StreamHandler()
handler.setFormatter(JSONFormatter())
logger.addHandler(handler)
logger.setLevel(logging.INFO)
# Us en el codi
logger.info("Crida a LLM", extra={"model": "gpt-4o-mini", "tokens": 150})
logger.warning("Cost acumulat elevat", extra={"cost_total_usd": 4.85})
6.3 Testing per a codi d'IA
Testejar codi d'IA es diferent de testejar codi convencional: les sortides dels LLMs son no-deterministes. Cal dissenyar tests robustos:
import pytest
from unittest.mock import patch, MagicMock
# Test d'una funcio de pre-processament (determinista, facil de testejar)
def test_netejar_text():
from src.utils import netejar_text
assert netejar_text(" Hola Mundo ") == "Hola Mundo"
assert netejar_text("") == ""
assert netejar_text("text\namb\nnoves\nlinies") == "text amb noves linies"
# Test d'una funcio que crida a un LLM (mock per evitar cost i no-determinisme)
def test_analitzar_sentiment_positiu():
from src.services import analitzar_sentiment
with patch('src.services.client.chat.completions.create') as mock_create:
# Simular resposta de l'API
mock_resposta = MagicMock()
mock_resposta.choices[0].message.content = "positiu"
mock_create.return_value = mock_resposta
resultat = analitzar_sentiment("Estic molt content amb el producte!")
assert resultat["sentiment"] == "positiu"
mock_create.assert_called_once()
# Test de propietats (property-based testing amb Hypothesis)
from hypothesis import given, strategies as st
@given(st.text(min_size=1, max_size=1000))
def test_chunking_sempre_retorna_llista(text):
from src.utils import chunk_text
resultat = chunk_text(text, chunk_size=200, overlap=50)
assert isinstance(resultat, list)
assert all(isinstance(chunk, str) for chunk in resultat)
# El text original ha d'estar contingut en els chunks (aproximadament)
text_reconstruït = " ".join(resultat)
assert len(text_reconstruït) >= len(text)
7. Script complet d'inferència amb Hugging Face
L'exemple seguent mostra un pipeline d'inferencia complet i professional:
#!/usr/bin/env python3
"""
Script d'inferencia multilingual amb Hugging Face.
Analitza sentiment i extreu entitats de textos en catala/espanyol/angles.
Requisits (requirements.txt):
transformers==4.44.0
torch==2.4.0
sentencepiece==0.2.0
protobuf==4.25.3
Docker:
docker run --rm -v $(pwd):/app python:3.11-slim bash -c \
"cd /app && pip install -r requirements.txt && python inferencia.py"
"""
import sys
import json
import logging
from pathlib import Path
from dataclasses import dataclass, field
from typing import Optional
import torch
from transformers import pipeline, Pipeline
# Configuracio de logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s | %(levelname)s | %(message)s",
datefmt="%Y-%m-%d %H:%M:%S"
)
logger = logging.getLogger(__name__)
@dataclass
class ConfigInferencia:
"""Configuracio del pipeline d'inferencia."""
model_sentiment: str = "pysentimiento/robertuito-sentiment-analysis"
model_ner: str = "dccuchile/bert-base-spanish-wwm-cased"
batch_size: int = 16
max_length: int = 512
usar_gpu: bool = field(default_factory=lambda: torch.cuda.is_available())
@property
def device(self) -> int:
return 0 if self.usar_gpu else -1
@dataclass
class ResultatAnalisi:
"""Resultat complet de l'analisi d'un text."""
text: str
sentiment: str
confianca_sentiment: float
idioma_detectat: Optional[str] = None
def a_dict(self) -> dict:
return {
"text": self.text[:100] + "..." if len(self.text) > 100 else self.text,
"sentiment": self.sentiment,
"confianca": round(self.confianca_sentiment, 4),
"idioma": self.idioma_detectat
}
class PipelineInferencia:
"""Pipeline d'inferencia reutilitzable."""
def __init__(self, config: ConfigInferencia):
self.config = config
self._pipeline_sentiment: Optional[Pipeline] = None
logger.info(f"Inicialitzant PipelineInferencia (GPU: {config.usar_gpu})")
def _carregar_sentiment(self) -> Pipeline:
"""Carrega el model de sentiment de manera lazy."""
if self._pipeline_sentiment is None:
logger.info(f"Carregant model: {self.config.model_sentiment}")
self._pipeline_sentiment = pipeline(
"text-classification",
model=self.config.model_sentiment,
device=self.config.device,
batch_size=self.config.batch_size
)
return self._pipeline_sentiment
def analitzar_batch(self, textos: list[str]) -> list[ResultatAnalisi]:
"""Analitza un batch de textos."""
if not textos:
return []
logger.info(f"Analitzant {len(textos)} textos...")
clf = self._carregar_sentiment()
# Truncar textos massa llargs
textos_truncats = [t[:self.config.max_length * 4] for t in textos]
resultats_raw = clf(
textos_truncats,
truncation=True,
max_length=self.config.max_length
)
resultats = []
for text, raw in zip(textos, resultats_raw):
# Normalitzar labels
label_map = {"POS": "positiu", "NEG": "negatiu", "NEU": "neutral"}
sentiment = label_map.get(raw["label"], raw["label"].lower())
resultats.append(ResultatAnalisi(
text=text,
sentiment=sentiment,
confianca_sentiment=raw["score"]
))
logger.info(f"Analisi completada. Resum: "
f"{sum(1 for r in resultats if r.sentiment == 'positiu')} positius, "
f"{sum(1 for r in resultats if r.sentiment == 'negatiu')} negatius, "
f"{sum(1 for r in resultats if r.sentiment == 'neutral')} neutres")
return resultats
def main():
# Textos d'exemple per a la demo
textos_demo = [
"El curs d'Intel·ligencia Artificial i Big Data es fantastic! Aprenc molt.",
"Avui el servidor ha caigut tres vegades. Es inacceptable per a un servei de produccio.",
"La reunio d'equip es dimarts a les 10h. Si us plau, confirmeu assistencia.",
"Estic impressionat amb les capacitats de Claude 3.5 Sonnet en tasques de codi.",
"L'API d'OpenAI te una latencia massa alta per als nostres requisits en temps real.",
"El model LLaMA 3.1 70B via Ollama ofereix resultats competitius sense cost d'API.",
]
config = ConfigInferencia(batch_size=8)
pipeline_ia = PipelineInferencia(config)
resultats = pipeline_ia.analitzar_batch(textos_demo)
# Mostrar resultats
print("\n" + "="*60)
print("RESULTATS DE L'ANALISI DE SENTIMENT")
print("="*60)
for r in resultats:
emoji = {"positiu": "✓", "negatiu": "✗", "neutral": "~"}[r.sentiment]
print(f"{emoji} [{r.sentiment:8s} {r.confianca_sentiment:.2%}] {r.text[:60]}...")
# Guardar resultats en JSON
output_path = Path("resultats_inferencia.json")
with open(output_path, "w", encoding="utf-8") as f:
json.dump([r.a_dict() for r in resultats], f, ensure_ascii=False, indent=2)
logger.info(f"Resultats guardats a {output_path}")
if __name__ == "__main__":
main()
Exercici pràctic
AC5073/01 — Entorn Python per a IA
Crea un projecte Python professional per a inferencia de sentiment que:
- Tingui un
Dockerfilebasada enpython:3.11-slim - Usi Pydantic per a la validacio de les dades d'entrada i sortida
- Implementi logging estructurat en format JSON
- Inclogui almenys 3 tests amb pytest (un unitari, un amb mock d'API, un amb Hypothesis)
- Contingui un
docker-compose.ymlque permeti executar-lo ambdocker compose up
El contenidor ha de tenir el nom sentiment-<el-teu-nom>-<primer-cognom> i mostrar el teu nom a les sortides de logging.
Preguntes de reflexio
- Per que es important usar
PYTHONDONTWRITEBYTECODE=1en un Dockerfile? - Quina diferencia hi ha entre
CMDiENTRYPOINTen un Dockerfile? - En quin cas preferiries usar Pandas 2.x amb backend PyArrow en lloc del backend per defecte (NumPy)?
- Explica el concepte de "lazy loading" que usa la classe
PipelineInferenciai per que es una bona practica.