NandaMatt's picture
commit to RepoB
93fc950 verified
Raw
History Blame Contribute Delete
10.3 kB
"""
Implementação concreta (PyTorch) do modelo treinado pelo pipeline.
Arquitetura: MLP simples para dados tabulares. A configuração de
camadas, dropout, otimizador, etc. vem de ``config`` (lido do
``config.json`` do treino) ou de defaults razoáveis.
A classe ``TabularMLP`` herda de ``BaseModel`` (em ``docker/base_model.py``)
para manter a API uniforme entre frameworks. Uma função ``create_model``
fábrica é exposta no fim do arquivo para que ``main.py`` continue
funcionando sem mudanças.
"""
from __future__ import annotations
import os
import sys
from typing import Any, Dict, List, Optional
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
# Permite importar base_model.py de ../
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
from base_model import BaseModel # noqa: E402
from utils import emit_progress # noqa: E402
# ---------------------------------------------------------------------------
# Arquitetura MLP
# ---------------------------------------------------------------------------
class _MLP(nn.Module):
def __init__(
self,
input_size: int,
hidden_dims: List[int],
num_classes: int,
dropout: float = 0.0,
) -> None:
super().__init__()
layers: List[nn.Module] = []
prev = input_size
for h in hidden_dims:
layers.append(nn.Linear(prev, h))
layers.append(nn.ReLU())
if dropout and dropout > 0:
layers.append(nn.Dropout(dropout))
prev = h
layers.append(nn.Linear(prev, num_classes))
self.net = nn.Sequential(*layers)
def forward(self, x: torch.Tensor) -> torch.Tensor: # noqa: D401
return self.net(x)
# ---------------------------------------------------------------------------
# Wrapper que implementa a API de BaseModel
# ---------------------------------------------------------------------------
class TabularMLP(BaseModel):
"""MLP tabular em PyTorch, plugável no pipeline."""
def build_model(self) -> nn.Module:
hp = self.config.get("hyperparameters", {})
hidden_dims = self.config.get("hidden_dims") or hp.get("hidden_dims") or [
self.config.get("hidden_dim1", 32),
self.config.get("hidden_dim2", 16),
]
dropout = float(hp.get("dropout", self.config.get("dropout", 0.0)))
# device
self.device = torch.device(
"cuda"
if torch.cuda.is_available() and self.config.get("use_cuda", True)
else "cpu"
)
model = _MLP(
input_size=self.input_size,
hidden_dims=list(hidden_dims),
num_classes=self.num_classes,
dropout=dropout,
).to(self.device)
self.criterion = nn.CrossEntropyLoss()
self._hidden_dims = list(hidden_dims)
self._dropout = dropout
return model
# ---------------- treinamento ----------------
def train_model(
self,
train_loader: DataLoader,
val_loader: Optional[DataLoader],
epochs: int,
lr: float,
**kwargs: Any,
) -> Dict[str, list]:
hp = self.config.get("hyperparameters", {})
epochs = int(hp.get("epochs", epochs))
lr = float(hp.get("learning_rate", lr))
weight_decay = float(hp.get("weight_decay", 0.0))
optimizer_name = str(hp.get("optimizer", "adam")).lower()
optimizer = self._build_optimizer(optimizer_name, lr, weight_decay)
history = {
"train_loss": [], "train_acc": [],
"val_loss": [], "val_acc": [],
}
n_batches = max(len(train_loader), 1)
total_train_steps = max(epochs * n_batches, 1)
best_val_acc = -float("inf")
best_epoch = -1
for epoch in range(1, epochs + 1):
self.model.train()
run_loss = 0.0
correct = 0
total = 0
for batch_idx, (X, y) in enumerate(train_loader, start=1):
X = X.to(self.device)
y = y.to(self.device)
optimizer.zero_grad()
logits = self.model(X)
loss = self.criterion(logits, y)
loss.backward()
optimizer.step()
run_loss += float(loss.item()) * X.size(0)
preds = logits.argmax(dim=1)
correct += int((preds == y).sum().item())
total += int(y.size(0))
global_step = (epoch - 1) * n_batches
inner_pct = int(global_step * 100 / total_train_steps)
emit_progress(inner_pct, total_train_steps)
train_loss = run_loss / max(total, 1)
train_acc = correct / max(total, 1)
history["train_loss"].append(round(train_loss, 6))
history["train_acc"].append(round(train_acc, 6))
val_loss, val_acc = (float("nan"), float("nan"))
if val_loader is not None:
val_metrics = self.evaluate(val_loader)
val_loss = val_metrics["loss"]
val_acc = val_metrics["accuracy"]
if val_acc > best_val_acc:
best_val_acc = val_acc
best_epoch = epoch
history["val_loss"].append(round(val_loss, 6))
history["val_acc"].append(round(val_acc, 6))
print(
f"Epoch [{epoch:>3}/{epochs}] "
f"train_loss={train_loss:.4f} train_acc={train_acc:.4f} "
f"val_loss={val_loss:.4f} val_acc={val_acc:.4f}"
)
history["best_epoch"] = best_epoch
history["best_val_acc"] = round(best_val_acc, 6) if best_epoch > 0 else None
self.history = history
return history
# ---------------- avaliação ----------------
def evaluate(self, data_loader: DataLoader) -> Dict[str, float]:
self.model.eval()
loss_sum = 0.0
correct = 0
total = 0
with torch.no_grad():
for X, y in data_loader:
X = X.to(self.device)
y = y.to(self.device)
logits = self.model(X)
loss = self.criterion(logits, y)
loss_sum += float(loss.item()) * X.size(0)
preds = logits.argmax(dim=1)
correct += int((preds == y).sum().item())
total += int(y.size(0))
return {
"loss": loss_sum / max(total, 1),
"accuracy": correct / max(total, 1),
"n": total,
}
# ---------------- inferência ----------------
def predict(self, inputs: Any) -> np.ndarray:
self.model.eval()
with torch.no_grad():
if isinstance(inputs, DataLoader):
outs: List[np.ndarray] = []
for batch in inputs:
X = batch[0] if isinstance(batch, (tuple, list)) else batch
X = X.to(self.device)
outs.append(self.model(X).argmax(dim=1).cpu().numpy())
return np.concatenate(outs, axis=0)
if isinstance(inputs, np.ndarray):
X = torch.from_numpy(inputs.astype(np.float32)).to(self.device)
elif isinstance(inputs, torch.Tensor):
X = inputs.to(self.device)
else:
X = torch.tensor(inputs, dtype=torch.float32).to(self.device)
if X.ndim == 1:
X = X.unsqueeze(0)
return self.model(X).argmax(dim=1).cpu().numpy()
# ---------------- persistência ----------------
def save_model(self, filename: str) -> str:
path = os.path.join(self.model_dir, filename)
torch.save(
{
"state_dict": self.model.state_dict(),
"input_size": self.input_size,
"num_classes": self.num_classes,
"hidden_dims": self._hidden_dims,
"dropout": self._dropout,
"project_name": self.project_name,
},
path,
)
print(f"✓ Model saved to {path}")
return path
def load_model(self, filename: str) -> None:
path = filename if os.path.isabs(filename) else os.path.join(
self.model_dir, filename
)
ckpt = torch.load(path, map_location=self.device)
self._hidden_dims = ckpt.get("hidden_dims", self._hidden_dims)
self._dropout = ckpt.get("dropout", self._dropout)
self.model = _MLP(
input_size=ckpt["input_size"],
hidden_dims=self._hidden_dims,
num_classes=ckpt["num_classes"],
dropout=self._dropout,
).to(self.device)
self.model.load_state_dict(ckpt["state_dict"])
# ---------------- helpers ----------------
def _build_optimizer(
self, name: str, lr: float, weight_decay: float
) -> optim.Optimizer:
params = self.model.parameters()
if name == "sgd":
return optim.SGD(params, lr=lr, momentum=0.9, weight_decay=weight_decay)
if name == "rmsprop":
return optim.RMSprop(params, lr=lr, weight_decay=weight_decay)
# default: adam
return optim.Adam(params, lr=lr, weight_decay=weight_decay)
# ---------------------------------------------------------------------------
# Fábrica usada pelo main.py
# ---------------------------------------------------------------------------
def create_model(
input_size: int,
num_classes: int,
project_name: str,
base_path: str,
config: Optional[Dict[str, Any]] = None,
) -> TabularMLP:
"""Cria um ``TabularMLP`` pronto pra uso pelo main.py."""
return TabularMLP(
input_size=input_size,
num_classes=num_classes,
project_name=project_name,
base_path=base_path,
framework="pytorch",
config=config or {},
)