Guía de Trabajo Práctico 1#
Materia: Aprendizaje Profundo Basado en la Física (optativa del Instituto Balseiro)
Docente: José I. Robledo
Edición: abril-mayo 2026
Parte 1 - Introducción a la programación orientada a objetos en Python#
Imports#
from __future__ import annotations
from random import Random
import matplotlib.pyplot as plt
import numpy as np
Ejercicio 1 - Clase Medicion#
Supongamos que queremos representar una medicion simple de un experimento. Para esto utilizaremos una clase y veremos la ventaja de la programación orientada a objetos que ofrece Python. Supongamos que medimos el valor de una variable en su correspondiente unidad (podría ser peso en Kg, temperatura en Kelvin, largo en cm, lo que se te ocurra).
Crear una clase
Medicioncon atributos:nombre_variable(str)valor(float)unidad(str)
Implementar:
__repr__para que la instancia sea facil de inspeccionares_positiva()que devuelvaTruesivalores un numero no negativoto_dict()que devuelva un diccionario con la informacion de la medición
Crear al menos 3 mediciones distintas y mostrarlas.
# TODO: definir la clase Medicion
# TODO: crear 3 instancias
# TODO: imprimirlas y verificar su correcto funcionamiento
Ejercicio 2 - Clase SerieDeMediciones#
Ahora queremos agrupar mediciones de una misma variable y exponer métodos útiles para la serie de mediciones. Además, queremos asegurar que sólo podamos agrupar mediciones del mismo tipo.
Crear una clase
SerieDeMedicionesque reciba:nombreuna lista opcional de objetos
Medicion
Implementar los métodos:
agregar(medicion): agrega instancia deMedicióna la lista de mediciones.eliminar(medicion): elimina una medición de la lista a partir de su índice posicional.valores(): devuelve una lista con los valores numericos de todas las mediciones en la lista.promedio(): calcula el promedio muestral de los valores numéricos de la lista de mediciones como \(\sum_{i=1}^{N} x_i /N\).desvio_estandar(): calcula el desvío estándar muestral como \(\sum_{i=1}^{N-1} (x_i - \bar x)^2/(N-1)\)minimo(): Calcula el mínimo de los valores numéricos de la lista de mediciones.maximo(): Calcula el máximo de los valores numéricos de la lsita de mediciones.resumen(): devuelve un diccionario con la cantidad de mediciones, el valor promedio, el mínimo y el máximo
Verificar que no se puedan agregar objetos de tipo incorrecto.
Crear una serie con al menos 5 mediciones de temperatura y probar todos los métodos implementados.
# TODO: definir SerieDeMediciones
# TODO: crear una serie de temperaturas
# TODO: probar los métodos implementados
Ejercicio 3 - Clase DatasetRegresion#
La dependencia de la presión de vapor de saturación del agua con la temperatura puede aproximarse, bajo la hipótesis de vapor ideal y calor latente constante, mediante la forma integrada de la ecuación de Clausius–Clapeyron:
donde:
(P) es la presión de vapor de saturación (Pa),
(T) es la temperatura absoluta (K),
(L) es la entalpía de vaporización (J/kg),
(R_v) es la constante de gas del vapor de agua (J/(kg·K)),
(C) es una constante relacionada con el estado.
Reordenando, se puede obtener una expresión aproximada para (T) en función de (P):
Usaremos esta relación para generar datos sintéticos de temperatura dependientes de la presión, y añadiremos un término adicional de ruido para hacer el dataset más realista y adecuado para un problema de regresión. Observando la tendencia de los datos, decidimos hacer una regresión 1D. Para el problema de regresión, necesitamos que cada muestra tenga:
una valor de
presioncomo variable medidaun valor de temperatura
Tcomo respuesta
Crear una clase
DatasetRegresionque almacene los datos. Cada muestra puede representarse como diccionario o como objeto propio.Implementar:
agregar_muestra(x, y)features()que devuelva la lista dextargets()que devuelva la lista dey__len__()train_test_split(test_size=0.2, seed=42)que separe al conjunto de datos en una parte de entrenamiento y otra de prueba y devuelva dos instancias deDatasetRegressionde las particiones.
Revisar la generación de dataset sintético con una regla aproximada tipo
T = L/(R_v*(C - np.log(P_pa + 1e-8))) - 273.15 + ruidopara presiones entre 850hPa y 1050hPa usandoruidoaleatorio uniforme entre -1.5 y 1.5. Para toda instancia aleatoria, tener en cuenta una semilla conRandom(seed).Separar el conjunto de datos generados en 75% para entrenamiento y 25% para prueba y mostrar cuantos elementos hay en cada uno.
# TODO: definir DatasetRegresion
# Generar datos sinteticos reproducibles
rng = Random(7)
presiones, temperaturas = [], []
for _ in range(100):
presion = rng.uniform(850, 1050)
presiones.append(presion)
ruido = rng.uniform(-1.5, 1.5)
# Relación inspirada en Clausius–Clapeyron (vapor de agua)
P_pa = presion * 100.0
L = 2.26e6 # J/kg, latente de vaporización del agua
R_v = 461.5 # J/(kg·K), gas constante del vapor de agua
C = 20.0
# Temperatura (K) del vapor saturado igualada con la ecuación de Clausius-Clapeyron
# y convertida a °C.
T_k = L / (R_v * (C - np.log(P_pa + 1e-8)))
temperatura = T_k - 273.15 + ruido
temperaturas.append(temperatura)
# TODO: hacer split train/test
# TODO: imprimir tamaños y algun ejemplo
plt.figure(figsize=(7, 4))
plt.scatter(presiones, temperaturas, alpha=0.7)
plt.xlabel('Presión (hPa)')
plt.ylabel('Temperatura (°C)')
plt.title('Temperatura vs Presión')
plt.grid(True)
plt.show()
Ejercicio 4 - Modelo con interfaz fit / predict#
Queremos una clase que aprenda una recta a partir de datos 1D usando una fórmula cerrada de mínimos cuadrados.
Crear una clase
RegresionLinealSimplecon atributospendienteeintercepto.Implementar:
fit(xs, ys)para estimar la rectay = m*x + butilizando las ecuaciones de mínimos cuadrados.predict(xs)que reciba una lista de entradas y devuelva una lista de prediccionespredict_one(x)que reciba un valor escalar y devuelva una predicción puntual
Entrenar el modelo sobre el dataset train del ejercicio anterior.
Mostrar
pendienteeinterceptoaprendidos.Graficar la predicción sobre el conjunto de datos de entrenamiento, y luego la predicción usando los datos de prueba.
Opcional: Realizar el ejercicio utilizando
LinearRegressiondescikit-learn.
Ecuaciones de Mínimos Cuadrados#
Media: $\(\bar x = \frac{1}{n} \sum_{i=1}^{n} x_i, \qquad \bar y = \frac{1}{n} \sum_{i=1}^{n} y_i\)$
Pendiente: $\(m = \frac{\sum_{i=1}^{n} (x_i - \bar x)(y_i - \bar y)}{\sum_{i=1}^{n} (x_i - \bar x)^2}\)$
Intercepto: $\(b = \bar y - m\,\bar x\)$
Predicción: $\(\hat y = m x + b\)$
# TODO: definir RegresionLinealSimple
# TODO: entrenar con train.features() y train.targets()
# TODO: imprimir parámetros y algunas predicciones
# TODO: graficar predicción con datos de entrenamiento y de prueba.
Ejercicio 5 - Clase MetricasRegresion#
Vamos a separar la lógica del modelo de la lógica de evaluación.
Crear una clase
MetricasRegresioncon métodos estáticos:mse(y_true, y_pred)mae(y_true, y_pred)r2(y_true, y_pred)
Evaluar el modelo del ejercicio 4 sobre el conjunto de test.
Mostrar los valores obtenidos con 4 decimales.
Definición de métricas#
Las métricas de regresión se definen de la siguiente manera:
Error Cuadrático Medio (MSE) $\( MSE = \frac{1}{n} \sum_{i=1}^{n} (y_i - \hat{y}_i)^2 \)$
Error Absoluto Medio (MAE) $\( MAE = \frac{1}{n} \sum_{i=1}^{n} |y_i - \hat{y}_i| \)$
Coeficiente de Determinación (R²) $\( R^2 = 1 - \frac{\sum_{i=1}^{n} (y_i - \hat{y}_i)^2}{\sum_{i=1}^{n} (y_i - \bar{y})^2} \)$
donde:
\(y_i\) son los valores reales
\(\hat{y}_i\) son las predicciones del modelo
\(\bar{y}\) es el promedio de los valores reales
\(n\) es la cantidad de muestras
# TODO: definir MetricasRegresion
# TODO: evaluar en test
# TODO: imprimir métricas con formato
Ejercicio 6 - Clase Experimento#
Crear una clase de más alto nivel que coordine dataset, modelo y resultados.
Crear una clase
ExperimentoRegresionque reciba:nombredataset_traindataset_testmodelo
Implementar un método
ejecutar()que:entrene el modelo
compute predicciones en train y test
guarde un atributo
historialcomo lista de diccionarios con métricas
Implementar
reporte()que devuelva un string legible con el resumen del experimento.Ejecutar el experimento y mostrar el reporte.
# TODO: definir ExperimentoRegresion
# TODO: ejecutar experimento y mostrar reporte
# TODO: inspeccionar historial
Parte 2 - Introduccion a NumPy y Matplotlib#
from __future__ import annotations
from random import Random
from time import perf_counter
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(42)
plt.rcParams["figure.figsize"] = (7, 4)
Ejercicio 7 - Obtener arrays desde las clases de mediciones y dataset#
Construir
temp_arraycomo array denumpya partir deserie_temp.valores().Obtener
x_train,y_train,x_test,y_testcomo arrays de numpy a partir detrain_dsytest_ds.Construir
XY_traincomo array de numpy de dimensión(n_train, 2)apilandox_trainey_trainpor columnas. Practicarslicing: para cada uno de los siguientes enunciados, crear una nueva variable a partir deXY_train(e imprimir su dimensión) que contenga:los primeros 20 elementos de
x.los últimos 20 elementos de
y.la multiplicación de los dos arrays anteriores mediante
*. Analizar qué significa esta operación.el logaritmo de los primeros 20 elementos de
x.los pares
(x,y)con índice par.el vector
y.un subconjunto aleatorio de 25 elementos
(x,y)dexy_train.
Imprimir para todos los arrays construídos:
shape,dtype,ndim,size.Verificar que:
la dimensión de
temp_arrayes 1:temp_array.ndim == 1los tamaños de
x_trainyy_trainson iguales:x_train.shape == y_train.shapeLa segunda dimensión de
XY_traincontiene dos entradas:XY_train.shape[1] == 2
# TODO: construir temp_array, x_train, y_train, x_test, y_test
# TODO: armar XY_train
# TODO: realizar operaciones con XY_train
# TODO: imprimir propiedades
# TODO: verificaciones
Ejercicio 8 - Beneficio de usar NumPy#
Vamos a comparar una operación equivalente hecha con listas de Python y con arrays de numpy, usando los datos anteriores.
Partir de
serie_temp.valores()y construir una lista largatemps_grandes_listarepitiendo esas mediciones muchas veces (por ejemplo 200_000 repeticiones).Construir
temps_grandes_arraycomonp.array(temps_grandes_lista).centrar los datos respecto al valor promedio (calcular y restar el valor promedio):
con listas Python
con NumPy
Medir tiempos con
perf_counter()para ambas variantes.Verificar con
np.allcloseque ambas producen el mismo resultado.Reportar:
tiempo con listas
tiempo con NumPy
factor aproximado de mejora
# TODO: crear temps_grandes_lista y temps_grandes_array
# TODO: calcular la version centrada con listas
# TODO: calcular la version centrada con NumPy
# TODO: medir tiempos y comparar resultados
Ejercicio 9 - Vectorización en NumPy#
Ya tenemos modelo entrenado en el Ejercicio 4. Ahora vamos a usar su pendiente e intercepto con numpy arrays.
Construir
y_hat_train_npyy_hat_test_npusando la formula vectorizada: $\( \hat{T} = m \cdot P + b \)$
donde m y b son la pendiente y el intercepto estimadas por el modelo y P el vector de entrenamiento en formato de array de numpy.
Obtener las predicciones del modelo para train y test.
Verificar que las predicciones vectorizadas y las del modelo coinciden.
Calcular el error cuadrático medio (MSE) para los conjuntos en entrenamiento y prueba en forma vectorizada.
Mostrar las primeras predicciones de ambos enfoques para train.
# TODO: usar pendiente e intercepto de modelo sobre arrays
# TODO: comparar contra predict del modelo
# TODO: calcular mse_train y mse_test vectorizados
# TODO: imprimir resultados
Ejercicio 10 - Reobtener el mismo ajuste con álgebra lineal de NumPy#
Veamos la facilidades provistas por numpy. Queremos reproducir el ajuste lineal usando numpy.linalg.lstsq.
Construir la matriz de diseño
A_train = [x_train, 1].Resolver el problema de mínimos cuadrados con
np.linalg.lstsq.Obtener
m_hat_npyb_hat_np.Comparar esos parámetros con
modelo.pendienteymodelo.interceptode los ejercicios anteriores.Verificar con
np.allcloseque ambos ajustes son equivalentes dentro de una tolerancia razonable.Calcular
y_hat_test_lstsqy elmae_test_lstsq.
# TODO: armar A_train, útil: np.ones_like
# TODO: resolver lstsq
# TODO: comparar parámetros con el ajuste de RegresionLinealSimple
# TODO: calcular predicciones y MAE en test
Ejercicio 11 - Visualizar datos y ajuste con Matplotlib#
Explorar la capacidad de la librería matplotlib para graficar.
Graficar los puntos de train y test en un mismo gráfico con colores distintos.
Superponer la recta del ajuste obtenido por el modelo de regresión inicial.
Agregar título, ejes y leyenda.
En una segunda figura, graficar los residuos en el conjunto de prueba: $\( r_i = y_i - \hat{y}_i \)$
Verificar visualmente si el ajuste parece razonable.
# TODO: scatter de train y test
# TODO: recta del ajuste de POO
# TODO: grafico de residuos en test
Parte 3 - Introduccion a PyTorch#
import torch
from torch import nn
from torch.utils.data import Dataset, DataLoader
torch.manual_seed(42)
print("torch version:", torch.__version__)
print("cuda available:", torch.cuda.is_available())
torch version: 2.6.0+cu124
cuda available: False
Ejercicio 12 — Tensores: creación, dtypes, shapes y dispositivos#
Crear los siguientes tensores:
t0: tensor 1D con valores [100, 0, 25.6] como (float32) usandotorch.tensora: tensor 1D con valores[0,1,2,...,9](int64) usandotorch.arangeb: tensor 2D de shape(3,4)con valores aleatorios uniformes en[0,1)(float32) utilizandotorch.randc: tensor 3D de shape(2,3,4)lleno de unos utilizandotorch.ones
Crear una función llamada
mostrar_infoque reciba un tensor e imprima sudtype,shape,device,requires_grad
Utilizar la función para cada tensor creado anteriormente.
Convertir
aafloat32utilizando el método de los tensores.to()y normalizarlo a media 0 y desvío 1 utilizando los métodos.mean()y.std().Mover
ba GPU si tiene GPU disponible utilizando el método.to(); si no, dejarlo en CPU.
# TODO: crear t0, a, b, c
# TODO: imprimir propiedades
# TODO: convertir/normalizar a
# TODO: mover b a CUDA si existe
Ejercicio 13 — Indexing, slicing, broadcasting y operaciones básicas#
Crear un tensor
Xcon valores0,1,...,29y reacomodarlo a dimensión(5, 6)utilizando el método.view()de los tensores de pytorch.Extraer:
la tercera fila (índice 2) completa
la quinta columna (índice 4) completa
un sub-bloque de 3x2 (filas 1..3, columnas 2..3)
Crear un vector
vde dimensión(6,)y sumarX + vusando broadcasting.Verificar con asserts:
que
X + vtiene shape(5,6)que la fila 0 de
X+vcoincide conX[0] + v
crear un tensor con elementos enteros aleatorios entre -10 y 10, de dimensión (10,8,2)
# TODO: crear X con shape (5,6)
# TODO: slicing solicitado
# TODO: broadcasting con v
# TODO: asserts
# TODO(opcional): view vs reshape vs transpose
Ejercicio 14 — Autograd: gradientes y verificación numérica simple#
Definir un tensor
xescalar conrequires_grad=Truee inicializarlo con algún valor (ej. 1.5).Definir la función: $\( f(x) = x^3 + 2x^2 - 5x + 1 \)$
Calcular
f(x)y obtenerdf/dxusandobackward().Verificar aproximadamente el gradiente usando diferencias finitas:
con epsilon = 1e-4.
Reportar el error absoluto entre autograd y diferencias finitas.
# TODO: definir x con requires_grad=True
# TODO: definir f(x) y backward
# TODO: gradiente por diferencias finitas
# TODO: comparar y reportar error absoluto
Ejercicio 15 — nn.Module: construir un modelo y contar parámetros#
Implementar una clase
MLP(heredando denn.Module) con:Linear(in_features=10, out_features=32)ReLULinear(32, 1)
Crear una instancia del modelo y:
imprimir el modelo
contar parámetros entrenables (total) y mostrar el número
Hacer un paso hacia adelante de la red (forward pass) con un batch sintético
Xde shape(16, 10)y chequear que la salida tiene shape(16, 1).Explicar brevemente (Markdown): diferencia entre
model.parameters()ystate_dict().
# TODO: definir MLP(nn.Module)
# TODO: instanciar, imprimir, contar parámetros
# TODO: forward con batch sintético y assert de shape
# TODO: breve explicación en Markdown (en celda aparte)
Ejercicio 16 — Entrenamiento básico: loop, optim, train()/eval()#
Vamos a entrenar un modelo simple en un dataset sintético de regresión: $\( y = 3x + 0.5 + \text{ruido} \)$ donde (x) es 1D.
Generar datos sintéticos:
N=512puntosxuniforme en[-2, 2]ruido Gaussiano con
std=0.2
Definir un modelo
nn.Linear(1,1).Elegir:
pérdida MSE (
nn.MSELoss)optimizador SGD (
torch.optim.SGD) conlrrazonable
Implementar loop de entrenamiento por
epochs=300:usar
model.train()zero_grad(), forward, loss,backward(),step()loggear la loss cada 50 épocas
Al final, imprimir los parámetros aprendidos (
weight,bias) y compararlos con (3, 0.5).(Opcional) correr un paso con
model.eval()ytorch.no_grad()para predecir 5 valores.
# TODO: generar dataset sintético (x, y) como tensores float32 con shape (N,1)
# TODO: definir modelo nn.Linear(1,1), loss y optimizer
# TODO: loop de entrenamiento con logs
# TODO: imprimir weight/bias aprendidos y comparar
Ejercicio 17 — Dataset y DataLoader: batching, shuffle y collation#
Crear una clase
ToyDataset(Dataset)que:reciba tensores
Xeyimplemente
__len__y__getitem__
Reutilizar los datos sintéticos del Ejercicio 16 y crear:
dataset = ToyDataset(X, y)loader = DataLoader(dataset, batch_size=64, shuffle=True)
Iterar sobre
loadery:imprimir la dimensión del primer batch
verificar que
X_batches(64,1)yy_batches(64,1)
Modificar el loop de entrenamiento del Ejercicio 16 para usar el
DataLoader(mini-batches).
# TODO: definir ToyDataset
# TODO: crear dataset y dataloader
# TODO: iterar un batch y verificar shapes
# TODO: re-entrenar el modelo usando mini-batches desde el loader
Ejercicio 18 — Guardar y cargar: state_dict, checkpoints y reproducibilidad#
Consignas#
Después de entrenar el modelo del ejercicio 17, guardar en un diccionario un checkpoint que incluya:
model_state_dictoptimizer_state_dictepochtrain_lossfinal (un float)
Guardar en un archivo:
checkpoint.pt.Crear un nuevo modelo (misma arquitectura) y un nuevo optimizador.
Cargar el checkpoint y restaurar estados.
Verificar que, con el modelo restaurado en
eval()yno_grad(), la predicción para un mismox_testcoincide (o es extremadamente cercana) con la del modelo original.
Nota: usar torch.save / torch.load y load_state_dict.
# TODO: armar diccionario checkpoint y guardar a 'checkpoint.pt'
# TODO: reinstanciar modelo/optimizer y cargar checkpoint
# TODO: verificar predicciones equivalentes en un x_test fijo
Ejercicio 19 — Modelado de la ley de enfriamiento mediante una red neuronal#
En este ejercicio aproximaremos la ley de enfriamiento de Netwon con una red neuronal. Ésta describe cómo varía la temperatura de un objeto en función del tiempo cuando se encuentra en un ambiente con temperatura constante. La ecuación del proceso es
en donde \(T(t)\) es la temperatura al tiempo \(t\), \(T_0\) la temperatura inicial, \(T_{amb}\) la ambiente y \(k\) la constante de enfriamiento.
Se busca entrenar un modelo de red neuronal (tipo MLP) que aprenda a aproximar esta función a partir de datos simulados con ruido.
Implementar una función que genere datos sintéticos a partir de la ley de enfriamiento de Newton. Agregar ruido gaussiano a las observaciones para simular mediciones reales y generar una base de datos de 1000 puntos. Visualizar los datos en un gráfico de dispersión.
Dividir los datos en entrenamiento y prueba y generar un dataloader con tamaño de batch de 32.
Implementar el modelo MLP en pytorch utilizando
torch.nn.Module. El modelo debe recibir como entrada el tiempo \(t\) y devolver la temperatura estimada. Utilizar al menos una capa oculta y una función de activación no lineal.Definir una función de pérdida adecuada para regresión y un optimizador utilizar una tasa de aprendizaje de \(0.002\). Implementar un loop de entrenamiento que haga un forward pass, calcule la pérdida, haga un backward pass, y actualice los parámetros. Entrenar el modelo con los datos de entrenamiento durante 4000 épocas
Mostrar el valor de la pérdida cada cierta cantidad de épocas y analizar si el modelo está aprendiendo correctamente.
Comparar visualmente las predicciones del modelo con los datos de prueba. Graficar la curva de pérdida.
# TODO: Implementar función que genere datos con ruido gaussiano
# Parámetros para generación de datos
T0 = 100.0 # Temperatura inicial
Tamb = 25.0 # Temperatura ambiente
k = 0.05 # Constante de enfriamiento
t_max = 100.0 # Tiempo máximo
n_puntos = 1000 # Número de puntos
# TODO: Dividir datos en entrenamiento y prueba
# TODO: definir modelo MLP en pytorch
# TODO: definir función de costo y optimizador
# TODO: escribir loop de entrenamiento teniendo en cuenta lo que resta del ejercicio
# TODO: Graficar resultados