| """ |
| models/train.py |
| Classe Trainer pour PyTorch. |
| Fonctionnalitรฉs : early stopping, ReduceLROnPlateau, |
| sauvegarde du meilleur modรจle, courbes train/val. |
| """ |
|
|
| import torch |
| import torch.nn as nn |
| from tqdm import tqdm |
| import matplotlib.pyplot as plt |
|
|
|
|
| class Trainer: |
| def __init__(self, model, train_dataloader, test_dataloader, |
| lr=1e-3, epochs=30, device="cpu", patience=5): |
| self.model = model |
| self.train_dataloader = train_dataloader |
| self.test_dataloader = test_dataloader |
| self.epochs = epochs |
| self.patience = patience |
| self.device = device |
| self.criterion = nn.CrossEntropyLoss() |
| self.optimizer = torch.optim.Adam(model.parameters(), lr=lr) |
| self.scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau( |
| self.optimizer, mode="min", |
| factor=0.5, patience=3) |
|
|
| |
| def train(self, save_path=None, plot=False): |
| self.train_loss, self.train_acc = [], [] |
| self.val_loss, self.val_acc = [], [] |
|
|
| best_val_loss = float("inf") |
| epochs_no_improve = 0 |
| best_state = None |
|
|
| for epoch in range(self.epochs): |
| tr_loss, tr_acc = self._train_one_epoch(epoch) |
| v_loss, v_acc = self._validate() |
|
|
| self.train_loss.append(tr_loss) |
| self.train_acc.append(tr_acc) |
| self.val_loss.append(v_loss) |
| self.val_acc.append(v_acc) |
|
|
| self.scheduler.step(v_loss) |
| lr = self.optimizer.param_groups[0]["lr"] |
|
|
| print(f"Epoch {epoch+1:02d}/{self.epochs} " |
| f"| Train loss={tr_loss:.4f} acc={tr_acc:.2f}% " |
| f"| Val loss={v_loss:.4f} acc={v_acc:.2f}% " |
| f"| LR={lr:.2e}") |
|
|
| |
| if v_loss < best_val_loss: |
| best_val_loss = v_loss |
| epochs_no_improve = 0 |
| best_state = {k: v.clone() for k, v in self.model.state_dict().items()} |
| if save_path: |
| torch.save(best_state, save_path) |
| print(f" โ Best model saved (val_loss={v_loss:.4f})") |
| else: |
| epochs_no_improve += 1 |
| print(f" โ No improvement {epochs_no_improve}/{self.patience}") |
| if epochs_no_improve >= self.patience: |
| print(f"\nโ Early stopping at epoch {epoch+1}") |
| break |
|
|
| if best_state: |
| self.model.load_state_dict(best_state) |
| if plot: |
| self.plot_history() |
|
|
| |
| def _train_one_epoch(self, epoch): |
| self.model.train() |
| total_loss, total_correct, total_samples = 0, 0, 0 |
| pbar = tqdm(self.train_dataloader, |
| desc=f"Epoch {epoch+1}/{self.epochs} [train]", leave=False) |
|
|
| for imgs, labels in pbar: |
| imgs, labels = imgs.to(self.device), labels.to(self.device) |
| self.optimizer.zero_grad() |
| out = self.model(imgs) |
| loss = self.criterion(out, labels) |
| loss.backward() |
| self.optimizer.step() |
|
|
| _, preds = out.max(1) |
| correct = (preds == labels).sum().item() |
| total = labels.size(0) |
| total_correct += correct |
| total_samples += total |
| total_loss += loss.item() |
|
|
| pbar.set_postfix({ |
| "Batch Acc": f"{100.*correct/total:.1f}%", |
| "Avg Acc": f"{100.*total_correct/total_samples:.1f}%", |
| "Loss": f"{total_loss/total_samples:.4f}", |
| }) |
|
|
| return total_loss / total_samples, 100. * total_correct / total_samples |
|
|
| |
| @torch.no_grad() |
| def _validate(self): |
| self.model.eval() |
| total_loss, total_correct, total_samples = 0, 0, 0 |
| for imgs, labels in self.test_dataloader: |
| imgs, labels = imgs.to(self.device), labels.to(self.device) |
| out = self.model(imgs) |
| loss = self.criterion(out, labels) |
| _, preds = out.max(1) |
| total_correct += (preds == labels).sum().item() |
| total_samples += labels.size(0) |
| total_loss += loss.item() * labels.size(0) |
| return total_loss / total_samples, 100. * total_correct / total_samples |
|
|
| |
| @torch.no_grad() |
| def evaluate(self): |
| loss, acc = self._validate() |
| print(f"\nTest Accuracy : {acc:.2f}% | Test Loss : {loss:.4f}") |
| return acc, loss |
|
|
| |
| def plot_history(self, save_path="/kaggle/working/history_pytorch.png"): |
| epochs = range(1, len(self.train_loss) + 1) |
| fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5)) |
|
|
| ax1.plot(epochs, self.train_loss, label="Train", color="tab:blue") |
| ax1.plot(epochs, self.val_loss, label="Val", color="tab:orange") |
| ax1.set_title("Loss"); ax1.set_xlabel("Epoch") |
| ax1.legend(); ax1.grid(alpha=.3) |
|
|
| ax2.plot(epochs, self.train_acc, label="Train", color="tab:blue") |
| ax2.plot(epochs, self.val_acc, label="Val", color="tab:orange") |
| ax2.set_title("Accuracy (%)"); ax2.set_xlabel("Epoch") |
| ax2.legend(); ax2.grid(alpha=.3) |
|
|
| fig.suptitle("Training History โ PyTorch", fontsize=13) |
| fig.tight_layout() |
| plt.savefig(save_path, dpi=120) |
| plt.show() |
| print(f"โ Courbes sauvegardรฉes โ {save_path}") |
|
|