File size: 7,751 Bytes
c04bc97
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
import os
import time
import torch
from tqdm import tqdm
from torch.cuda.amp import autocast, GradScaler

# =====================================================================
# 1. Epoch Level Training & Validation Loops
# =====================================================================

def train_epoch_standard(model, loader, optimizer, criterion, device, scaler=None):
    """
    Trains standard models (Classification/Segmentation) for one epoch.
    Supports Automatic Mixed Precision (AMP) when scaler is provided.
    """
    model.train()
    running_loss = 0.0
    correct = 0
    total = 0
    
    for x, y in tqdm(loader, desc="Training Batches", leave=False):
        # MedMNIST targets are often squeezed to shape (batch_size, 1), we need shape (batch_size,)
        if len(y.shape) == 2 and y.shape[1] == 1:
            y = y.squeeze(1)
            
        x, y = x.to(device), y.to(device)
        optimizer.zero_grad()
        
        if scaler is not None:
            with autocast():
                outputs = model(x)
                loss = criterion(outputs, y)
            scaler.scale(loss).backward()
            scaler.step(optimizer)
            scaler.update()
        else:
            outputs = model(x)
            loss = criterion(outputs, y)
            loss.backward()
            optimizer.step()
            
        running_loss += loss.item() * x.size(0)
        
        # Classification metric collection
        if len(outputs.shape) > 1 and outputs.shape[1] > 1:
            _, preds = torch.max(outputs, 1)
            correct += torch.sum(preds == y.data).item()
            total += y.size(0)
            
    epoch_loss = running_loss / len(loader.dataset)
    epoch_acc = (correct / total) if total > 0 else 0.0
    return epoch_loss, epoch_acc

@torch.no_grad()
def val_epoch_standard(model, loader, criterion, device):
    """
    Validates standard models (Classification/Segmentation) for one epoch.
    """
    model.eval()
    running_loss = 0.0
    correct = 0
    total = 0
    
    for x, y in tqdm(loader, desc="Validation Batches", leave=False):
        if len(y.shape) == 2 and y.shape[1] == 1:
            y = y.squeeze(1)
            
        x, y = x.to(device), y.to(device)
        outputs = model(x)
        loss = criterion(outputs, y)
        
        running_loss += loss.item() * x.size(0)
        
        if len(outputs.shape) > 1 and outputs.shape[1] > 1:
            _, preds = torch.max(outputs, 1)
            correct += torch.sum(preds == y.data).item()
            total += y.size(0)
            
    epoch_loss = running_loss / len(loader.dataset)
    epoch_acc = (correct / total) if total > 0 else 0.0
    return epoch_loss, epoch_acc


def train_epoch_detector(model, loader, optimizer, device, scaler=None):
    """
    Trains Faster R-CNN detection model for one epoch.
    In PyTorch torchvision, Faster R-CNN outputs a loss dict when in training mode.
    """
    model.train()
    running_loss = 0.0
    
    for images, targets in tqdm(loader, desc="Detection Batches", leave=False):
        # Images: list of Tensors
        # Targets: list of dicts: {"boxes": Tensor, "labels": Tensor}
        images = [img.to(device) for img in images]
        targets = [{k: v.to(device) for k, v in t.items()} for t in targets]
        
        optimizer.zero_grad()
        
        if scaler is not None:
            with autocast():
                loss_dict = model(images, targets)
                losses = sum(loss for loss in loss_dict.values())
            scaler.scale(losses).backward()
            scaler.step(optimizer)
            scaler.update()
        else:
            loss_dict = model(images, targets)
            losses = sum(loss for loss in loss_dict.values())
            losses.backward()
            optimizer.step()
            
        running_loss += losses.item() * len(images)
        
    epoch_loss = running_loss / len(loader.dataset)
    return epoch_loss, 0.0


# =====================================================================
# 2. Complete Training Runner with Checkpointing & Early Stopping
# =====================================================================

def fit_model(
    model, 
    train_loader, 
    val_loader, 
    optimizer, 
    criterion, 
    epochs, 
    device, 
    checkpoint_path, 
    scheduler=None, 
    early_stopping_patience=5,
    task_type="classification"
):
    """
    General runner function to train and validate a model.
    """
    best_val_loss = float("inf")
    patience_counter = 0
    history = {
        "train_loss": [],
        "train_acc": [],
        "val_loss": [],
        "val_acc": [],
        "epoch_time": []
    }
    
    # Enable mixed precision scaler if running on CUDA
    scaler = GradScaler() if device == "cuda" else None
    
    print(f"[Training] Starting training on device: {device} (Task: {task_type})")
    
    for epoch in range(1, epochs + 1):
        start_time = time.time()
        
        # 1. Run training epoch
        if task_type == "detection":
            train_loss, train_acc = train_epoch_detector(model, train_loader, optimizer, device, scaler)
            val_loss, val_acc = 0.0, 0.0  # Detection model doesn't compute validation loss this way natively
        else:
            train_loss, train_acc = train_epoch_standard(model, train_loader, optimizer, criterion, device, scaler)
            val_loss, val_acc = val_epoch_standard(model, val_loader, criterion, device)
            
        epoch_time = time.time() - start_time
        
        # Log metrics
        history["train_loss"].append(train_loss)
        history["train_acc"].append(train_acc)
        history["val_loss"].append(val_loss)
        history["val_acc"].append(val_acc)
        history["epoch_time"].append(epoch_time)
        
        # Print summary
        if task_type == "detection":
            print(f"Epoch {epoch}/{epochs} | Train Loss: {train_loss:.4f} | Time: {epoch_time:.1f}s")
        else:
            print(f"Epoch {epoch}/{epochs} | Train Loss: {train_loss:.4f} | Train Acc: {train_acc:.4f} | Val Loss: {val_loss:.4f} | Val Acc: {val_acc:.4f} | Time: {epoch_time:.1f}s")
            
        # Update Scheduler
        if scheduler is not None:
            # Check if ReduceLROnPlateau or standard scheduler
            if isinstance(scheduler, torch.optim.lr_scheduler.ReduceLROnPlateau):
                scheduler.step(val_loss if task_type != "detection" else train_loss)
            else:
                scheduler.step()
                
        # 2. Checkpoint Saving & Early Stopping
        if task_type == "detection":
            # For detection, we save based on train loss improvement
            metric_to_check = train_loss
        else:
            metric_to_check = val_loss
            
        if metric_to_check < best_val_loss:
            best_val_loss = metric_to_check
            patience_counter = 0
            # Save checkpoint
            torch.save(model.state_dict(), checkpoint_path)
            print(f" => State saved! New best loss: {best_val_loss:.4f}")
        else:
            patience_counter += 1
            if patience_counter >= early_stopping_patience:
                print(f"[Training] Early stopping triggered. Validation performance has not improved for {early_stopping_patience} epochs.")
                break
                
    # 3. Restore Best Checkpoint
    if os.path.exists(checkpoint_path):
        print(f"[Training] Restoring best checkpoint from: {checkpoint_path}")
        model.load_state_dict(torch.load(checkpoint_path, map_location=device))
    else:
        print("[Training Warning] No checkpoint saved, returning current model state.")
        
    return history