natisol87 commited on
Commit
d2e67f5
·
verified ·
1 Parent(s): f181b0b

Upload 8 files

Browse files
Files changed (8) hide show
  1. README.md +107 -0
  2. config.json +21 -0
  3. model.safetensors +3 -0
  4. modelo.py +16 -0
  5. predict.py +34 -0
  6. predict_hub.py +41 -0
  7. requirements.txt +6 -0
  8. train.py +83 -0
README.md ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ language:
3
+ - es
4
+ library_name: pytorch
5
+ pipeline_tag: tabular-regression
6
+ tags:
7
+ - machine-learning
8
+ - regression
9
+ - linear-regression
10
+ - safetensors
11
+ - educational-example
12
+ license: mit
13
+ ---
14
+
15
+ # Regresión lineal con PyTorch: precio de una casa
16
+
17
+ ## Descripción del modelo
18
+
19
+ Este modelo es un ejemplo pedagógico del Seminario de Actualización (IFTS N.º 18). Muestra cómo se entrena una regresión lineal con el ciclo explícito de PyTorch y cómo se publican los pesos en formato **safetensors**.
20
+
21
+ Predice el precio de una casa, en miles de US$, a partir de su superficie y de si tiene balcón y ascensor. Resuelve el mismo problema que el ejemplo de scikit-learn.
22
+
23
+ ## Objetivo
24
+
25
+ Predecir una variable numérica continua: el precio estimado de una casa.
26
+
27
+ ## Datos de entrenamiento
28
+
29
+ - **Dataset:** sintético, generado en `train.py`. Es el mismo de la Clase 10, con la misma semilla, y no son casas reales.
30
+ - **Cantidad de registros:** 200. Se usan 160 para entrenar y 40 para evaluar.
31
+ - **Variables de entrada (X):** `m2` (de 35 a 120), `balcon` (0 o 1), `ascensor` (0 o 1).
32
+ - **Variable objetivo (y):** `precio`, en miles de US$.
33
+ - **Preprocesamiento:** las entradas se normalizan restando la media y dividiendo por el desvío, ambos calculados con los datos de entrenamiento. Esos valores están en `config.json`.
34
+
35
+ ## Entrenamiento
36
+
37
+ - **Tipo de aprendizaje:** supervisado.
38
+ - **Problema:** regresión.
39
+ - **Arquitectura:** una capa `torch.nn.Linear(3, 1)`, es decir, tres pesos y un bias. Está definida en `modelo.py`.
40
+ - **Función de pérdida:** error cuadrático medio (`MSELoss`).
41
+ - **Optimizador:** descenso por gradiente (`SGD`), con tasa de aprendizaje 0,1.
42
+ - **Épocas:** 500. La pérdida se estabiliza antes de la época 100.
43
+ - **Biblioteca:** PyTorch.
44
+
45
+ **Parámetros aprendidos.** Son los que guarda `model.safetensors`:
46
+
47
+ | Parámetro | Valor |
48
+ |---|---|
49
+ | `lineal.weight` | [35,157 · 4,351 · 5,992] |
50
+ | `lineal.bias` | 147,769 |
51
+
52
+ **Por qué los pesos no se parecen a los de scikit-learn.** Están en la escala de las entradas **normalizadas**. Si se divide cada peso por el desvío de su variable, se vuelve a la escala original. Por ejemplo, 35,157 / 23,398 ≈ 1,503 para `m2`, que es el mismo peso que encontró scikit-learn.
53
+
54
+ ## Evaluación
55
+
56
+ Se evaluó sobre 40 casas que el modelo no vio durante el entrenamiento.
57
+
58
+ | Métrica | Resultado |
59
+ |---|---|
60
+ | MAE | 3,08 miles de US$ |
61
+ | R² | 0,989 |
62
+
63
+ ## Archivos
64
+
65
+ | Archivo | Qué es |
66
+ |---|---|
67
+ | `model.safetensors` | **solo los pesos** (`state_dict`) |
68
+ | `config.json` | las entradas, la salida y la normalización que hay que aplicar antes de predecir |
69
+ | `modelo.py` | la clase del modelo: PyTorch no la guarda en el archivo de pesos |
70
+ | `train.py` | el código que lo entrenó |
71
+ | `predict.py` | cómo cargarlo y usarlo |
72
+ | `requirements.txt` | las dependencias |
73
+ | `README.md` | esta Model Card |
74
+
75
+ ## Cómo utilizar el modelo
76
+
77
+ ```python
78
+ import json
79
+ import numpy as np
80
+ import torch
81
+ from safetensors.torch import load_file
82
+ from modelo import RegresionLineal
83
+
84
+ config = json.load(open("config.json"))
85
+ media = np.array(config["normalizacion"]["media"], dtype="float32")
86
+ desvio = np.array(config["normalizacion"]["desvio"], dtype="float32")
87
+
88
+ modelo = RegresionLineal(n_entradas=3) # 1. el modelo vacío
89
+ modelo.load_state_dict(load_file("model.safetensors")) # 2. se le cargan los pesos
90
+ modelo.eval()
91
+
92
+ casa = np.array([[80, 1, 0]], dtype="float32")
93
+ with torch.no_grad():
94
+ print(modelo(torch.tensor((casa - media) / desvio))) # tensor([[148.5]])
95
+ ```
96
+
97
+ ## Limitaciones
98
+
99
+ - Tiene fines exclusivamente educativos. Los datos son sintéticos y no representan precios reales.
100
+ - Solo conoce casas de 35 a 120 m².
101
+ - **Si se olvida la normalización, las predicciones dan cualquier cosa.** Los pesos solos no alcanzan: hace falta `config.json`.
102
+ - Para cargar los pesos hace falta la misma arquitectura (`modelo.py`). Si cambia la clase, `load_state_dict` falla.
103
+ - A diferencia de pickle o joblib, **safetensors no ejecuta código al cargarse**: solo contiene números.
104
+
105
+ ## Autoría
106
+
107
+ Modelo desarrollado con fines educativos para el IFTS N.º 18. Docente: Cynthia M. Villagra.
config.json ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "arquitectura": "RegresionLineal (torch.nn.Linear)",
3
+ "entradas": [
4
+ "m2",
5
+ "balcon",
6
+ "ascensor"
7
+ ],
8
+ "salida": "precio (miles de US$)",
9
+ "normalizacion": {
10
+ "media": [
11
+ 78.51875305175781,
12
+ 0.46875,
13
+ 0.512499988079071
14
+ ],
15
+ "desvio": [
16
+ 23.397640228271484,
17
+ 0.4990224838256836,
18
+ 0.4998437762260437
19
+ ]
20
+ }
21
+ }
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4f87d90c7f81d67c5ce4da4e585bad567d702ff36ebffe9e3e8747fb0f9e93ea
3
+ size 160
modelo.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Arquitectura del modelo. La usan train.py y predict.py.
2
+
3
+ PyTorch no guarda la clase dentro del archivo de pesos: para cargar model.safetensors
4
+ hay que volver a crear el mismo modelo vacío y después cargarle los pesos.
5
+ """
6
+
7
+ import torch
8
+
9
+
10
+ class RegresionLineal(torch.nn.Module):
11
+ def __init__(self, n_entradas=3):
12
+ super().__init__()
13
+ self.lineal = torch.nn.Linear(n_entradas, 1) # pesos (w) y bias (b)
14
+
15
+ def forward(self, x):
16
+ return self.lineal(x)
predict.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Carga model.safetensors + config.json y predice el precio de casas nuevas.
2
+
3
+ Uso:
4
+ python predict.py
5
+ """
6
+
7
+ import json
8
+ from pathlib import Path
9
+
10
+ import numpy as np
11
+ import torch
12
+ from safetensors.torch import load_file
13
+
14
+ from modelo import RegresionLineal
15
+
16
+ CARPETA = Path(__file__).parent
17
+
18
+ # 1. Leer la configuración (entradas y normalización)
19
+ config = json.loads((CARPETA / "config.json").read_text(encoding="utf-8"))
20
+ media = np.array(config["normalizacion"]["media"], dtype="float32")
21
+ desvio = np.array(config["normalizacion"]["desvio"], dtype="float32")
22
+
23
+ # 2. Crear el modelo vacío y cargarle los pesos entrenados
24
+ modelo = RegresionLineal(n_entradas=len(config["entradas"]))
25
+ modelo.load_state_dict(load_file(CARPETA / "model.safetensors"))
26
+ modelo.eval()
27
+
28
+ # 3. Predecir: las entradas se normalizan igual que en el entrenamiento
29
+ casas_nuevas = np.array([[50, 0, 0], [80, 1, 0], [110, 1, 1]], dtype="float32")
30
+ with torch.no_grad():
31
+ precios = modelo(torch.tensor((casas_nuevas - media) / desvio)).numpy().ravel()
32
+
33
+ for casa, precio in zip(casas_nuevas, precios):
34
+ print(f"{dict(zip(config['entradas'], casa.astype(int).tolist()))} -> {precio:.1f} miles de US$")
predict_hub.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Descarga el modelo desde Hugging Face Hub y predice EN ESTA PC (o en el servidor donde corra).
2
+
3
+ En PyTorch del Hub se bajan los pesos y la configuración, pero la arquitectura
4
+ (la clase RegresionLineal) tiene que estar en tu código: por eso se importa modelo.py.
5
+
6
+ Uso:
7
+ python predict_hub.py tu-usuario/regresion-casas-pytorch
8
+ """
9
+
10
+ import json
11
+ import sys
12
+
13
+ import numpy as np
14
+ import torch
15
+ from huggingface_hub import hf_hub_download
16
+ from safetensors.torch import load_file
17
+
18
+ from modelo import RegresionLineal
19
+
20
+ REPO_ID = sys.argv[1] if len(sys.argv) > 1 else "TU-USUARIO/regresion-casas-pytorch"
21
+
22
+ # 1. Bajar los dos archivos que hacen falta
23
+ ruta_pesos = hf_hub_download(repo_id=REPO_ID, filename="model.safetensors")
24
+ ruta_config = hf_hub_download(repo_id=REPO_ID, filename="config.json")
25
+
26
+ # 2. Armar el modelo vacío, cargarle los pesos y leer la normalización
27
+ with open(ruta_config, encoding="utf-8") as f:
28
+ config = json.load(f)
29
+ media = np.array(config["normalizacion"]["media"], dtype="float32")
30
+ desvio = np.array(config["normalizacion"]["desvio"], dtype="float32")
31
+
32
+ modelo = RegresionLineal(n_entradas=len(config["entradas"]))
33
+ modelo.load_state_dict(load_file(ruta_pesos))
34
+ modelo.eval()
35
+
36
+ # 3. Predecir
37
+ casas_nuevas = np.array([[50, 0, 0], [80, 1, 0], [110, 1, 1]], dtype="float32")
38
+ with torch.no_grad():
39
+ precios = modelo(torch.tensor((casas_nuevas - media) / desvio)).numpy().ravel()
40
+ for casa, precio in zip(casas_nuevas, precios):
41
+ print(f"{dict(zip(config['entradas'], casa.astype(int).tolist()))} -> {precio:.1f} miles de US$")
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ torch>=2.1
2
+ safetensors
3
+ numpy
4
+ pandas
5
+ scikit-learn # solo lo usa train.py, para separar train/test y medir
6
+ huggingface_hub # para descargar el modelo desde el Hub o publicarlo
train.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Entrena una regresión lineal con PyTorch y la exporta a model.safetensors + config.json.
2
+
3
+ Mismo problema que el ejemplo 01: precio de una casa a partir de m², balcón y ascensor.
4
+
5
+ Diferencia importante con scikit-learn: PyTorch guarda solo los pesos (state_dict).
6
+ La clase del modelo y la normalización de las entradas no viajan en el archivo de pesos,
7
+ así que la normalización se guarda aparte, en config.json.
8
+
9
+ Uso:
10
+ python train.py
11
+ """
12
+
13
+ import json
14
+ from pathlib import Path
15
+
16
+ import numpy as np
17
+ import pandas as pd
18
+ import torch
19
+ from safetensors.torch import save_file
20
+ from sklearn.metrics import mean_absolute_error, r2_score
21
+ from sklearn.model_selection import train_test_split
22
+
23
+ from modelo import RegresionLineal
24
+
25
+ CARPETA = Path(__file__).parent
26
+ ENTRADAS = ["m2", "balcon", "ascensor"]
27
+ torch.manual_seed(42)
28
+
29
+
30
+ def generar_casas(n=200, semilla=42):
31
+ """Dataset sintético de la Clase 10 (misma semilla, mismos datos)."""
32
+ rng = np.random.default_rng(semilla)
33
+ m2 = rng.integers(35, 121, n)
34
+ balcon = rng.integers(0, 2, n)
35
+ ascensor = rng.integers(0, 2, n)
36
+ precio = 1.5 * m2 + 8 * balcon + 12 * ascensor + 20 + rng.normal(0, 4, n)
37
+ return pd.DataFrame({"m2": m2, "balcon": balcon, "ascensor": ascensor, "precio": precio.round(1)})
38
+
39
+
40
+ # 1. Datos
41
+ casas = generar_casas()
42
+ X = casas[ENTRADAS].to_numpy(dtype="float32")
43
+ y = casas["precio"].to_numpy(dtype="float32").reshape(-1, 1)
44
+ X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
45
+
46
+ # Normalización: se calcula SOLO con los datos de entrenamiento y se guarda para predecir
47
+ media = X_train.mean(axis=0)
48
+ desvio = X_train.std(axis=0)
49
+ X_train_t = torch.tensor((X_train - media) / desvio)
50
+ y_train_t = torch.tensor(y_train)
51
+
52
+ # 2. Modelo, función de pérdida y optimizador
53
+ modelo = RegresionLineal(n_entradas=len(ENTRADAS))
54
+ perdida_fn = torch.nn.MSELoss()
55
+ optimizador = torch.optim.SGD(modelo.parameters(), lr=0.1)
56
+
57
+ # 3. El ciclo de entrenamiento
58
+ for epoca in range(500):
59
+ prediccion = modelo(X_train_t) # el modelo predice
60
+ perdida = perdida_fn(prediccion, y_train_t) # la pérdida mide el error
61
+ optimizador.zero_grad()
62
+ perdida.backward() # se calculan los gradientes
63
+ optimizador.step() # el optimizador corrige pesos y bias
64
+ if epoca % 100 == 0:
65
+ print(f"época {epoca:3d} · pérdida (MSE) {perdida.item():.2f}")
66
+
67
+ # 4. Evaluación con datos que el modelo no vio
68
+ modelo.eval()
69
+ with torch.no_grad():
70
+ pred_test = modelo(torch.tensor((X_test - media) / desvio)).numpy()
71
+ print(f"MAE en test: {mean_absolute_error(y_test, pred_test):.2f} miles de US$")
72
+ print(f"R² en test: {r2_score(y_test, pred_test):.3f}")
73
+
74
+ # 5. Exportación: pesos en safetensors, todo lo demás en config.json
75
+ save_file(modelo.state_dict(), CARPETA / "model.safetensors")
76
+ config = {
77
+ "arquitectura": "RegresionLineal (torch.nn.Linear)",
78
+ "entradas": ENTRADAS,
79
+ "salida": "precio (miles de US$)",
80
+ "normalizacion": {"media": media.tolist(), "desvio": desvio.tolist()},
81
+ }
82
+ (CARPETA / "config.json").write_text(json.dumps(config, indent=2, ensure_ascii=False), encoding="utf-8")
83
+ print("Modelo guardado en model.safetensors + config.json")