image-classification-tools 0.5.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- image_classification_tools/README.md +152 -0
- image_classification_tools/__init__.py +6 -0
- image_classification_tools/pytorch/__init__.py +46 -0
- image_classification_tools/pytorch/data.py +416 -0
- image_classification_tools/pytorch/evaluation.py +38 -0
- image_classification_tools/pytorch/hyperparameter_optimization.py +315 -0
- image_classification_tools/pytorch/plotting.py +311 -0
- image_classification_tools/pytorch/training.py +256 -0
- image_classification_tools/tensorflow/__init__.py +0 -0
- image_classification_tools-0.5.6.dist-info/METADATA +187 -0
- image_classification_tools-0.5.6.dist-info/RECORD +13 -0
- image_classification_tools-0.5.6.dist-info/WHEEL +4 -0
- image_classification_tools-0.5.6.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# Image Classification Tools
|
|
2
|
+
|
|
3
|
+
A lightweight PyTorch toolkit for building and training image classification models.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This package provides utilities for common image classification tasks:
|
|
8
|
+
|
|
9
|
+
- **Data loading**: Flexible data loaders for torchvision datasets and custom image folders
|
|
10
|
+
- **Model training**: Training loops with progress tracking and validation
|
|
11
|
+
- **Evaluation**: Accuracy metrics, confusion matrices, and performance analysis
|
|
12
|
+
- **Visualization**: Learning curves, probability distributions, and evaluation plots
|
|
13
|
+
- **Hyperparameter optimization**: Optuna integration for automated model tuning
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install image-classification-tools
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
### Basic usage
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
import torch
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from torchvision import datasets, transforms
|
|
29
|
+
from image_classification_tools.pytorch.data import (
|
|
30
|
+
load_datasets, prepare_splits, create_dataloaders
|
|
31
|
+
)
|
|
32
|
+
from image_classification_tools.pytorch.training import train_model
|
|
33
|
+
from image_classification_tools.pytorch.evaluation import evaluate_model
|
|
34
|
+
|
|
35
|
+
# Define transforms
|
|
36
|
+
transform = transforms.Compose([
|
|
37
|
+
transforms.ToTensor(),
|
|
38
|
+
transforms.Normalize((0.5,), (0.5,))
|
|
39
|
+
])
|
|
40
|
+
|
|
41
|
+
# Load datasets
|
|
42
|
+
train_dataset, test_dataset = load_datasets(
|
|
43
|
+
data_source=datasets.MNIST,
|
|
44
|
+
train_transform=transform,
|
|
45
|
+
eval_transform=transform,
|
|
46
|
+
download=True,
|
|
47
|
+
root=Path('./data/mnist')
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Prepare splits
|
|
51
|
+
train_dataset, val_dataset, test_dataset = prepare_splits(
|
|
52
|
+
train_dataset=train_dataset,
|
|
53
|
+
test_dataset=test_dataset,
|
|
54
|
+
train_val_split=0.8
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Create dataloaders
|
|
58
|
+
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
|
59
|
+
train_loader, val_loader, test_loader = create_dataloaders(
|
|
60
|
+
train_dataset, val_dataset, test_dataset,
|
|
61
|
+
batch_size=64,
|
|
62
|
+
preload_to_memory=True,
|
|
63
|
+
device=device
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Define model, criterion, optimizer
|
|
67
|
+
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
|
68
|
+
|
|
69
|
+
model = torch.nn.Sequential(
|
|
70
|
+
torch.nn.Flatten(),
|
|
71
|
+
torch.nn.Linear(784, 128),
|
|
72
|
+
torch.nn.ReLU(),
|
|
73
|
+
torch.nn.Linear(128, 10)
|
|
74
|
+
).to(device)
|
|
75
|
+
|
|
76
|
+
criterion = torch.nn.CrossEntropyLoss()
|
|
77
|
+
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
|
|
78
|
+
|
|
79
|
+
# Train with lazy loading (moves batches to device during training)
|
|
80
|
+
history = train_model(
|
|
81
|
+
model=model,
|
|
82
|
+
train_loader=train_loader,
|
|
83
|
+
val_loader=val_loader,
|
|
84
|
+
criterion=criterion,
|
|
85
|
+
optimizer=optimizer,
|
|
86
|
+
device=device,
|
|
87
|
+
lazy_loading=True, # Set False if data already on device
|
|
88
|
+
epochs=10
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Evaluate
|
|
92
|
+
accuracy, predictions, labels = evaluate_model(model, test_loader)
|
|
93
|
+
print(f'Test accuracy: {accuracy:.2f}%')
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Hyperparameter optimization
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from image_classification_tools.pytorch.hyperparameter_optimization import create_objective
|
|
100
|
+
import optuna
|
|
101
|
+
|
|
102
|
+
# Define search space
|
|
103
|
+
search_space = {
|
|
104
|
+
'batch_size': [32, 64, 128],
|
|
105
|
+
'n_conv_blocks': (1, 3),
|
|
106
|
+
'initial_filters': [16, 32, 64],
|
|
107
|
+
'n_fc_layers': (1, 3),
|
|
108
|
+
'conv_dropout_rate': (0.1, 0.5),
|
|
109
|
+
'fc_dropout_rate': (0.3, 0.7),
|
|
110
|
+
'learning_rate': (1e-4, 1e-2, 'log'),
|
|
111
|
+
'optimizer': ['Adam', 'SGD'],
|
|
112
|
+
'weight_decay': (1e-6, 1e-3, 'log')
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
# Create objective function
|
|
116
|
+
objective = create_objective(
|
|
117
|
+
data_dir='./data',
|
|
118
|
+
train_transform=transform,
|
|
119
|
+
eval_transform=transform,
|
|
120
|
+
n_epochs=20,
|
|
121
|
+
device=device,
|
|
122
|
+
num_classes=10,
|
|
123
|
+
in_channels=1,
|
|
124
|
+
search_space=search_space
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Run optimization
|
|
128
|
+
study = optuna.create_study(direction='maximize')
|
|
129
|
+
study.optimize(objective, n_trials=50)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Requirements
|
|
133
|
+
|
|
134
|
+
- Python ≥ 3.10
|
|
135
|
+
- PyTorch ≥ 2.0.0
|
|
136
|
+
- torchvision ≥ 0.15.0
|
|
137
|
+
- numpy
|
|
138
|
+
- matplotlib
|
|
139
|
+
- optuna (optional, for hyperparameter optimization)
|
|
140
|
+
|
|
141
|
+
## Documentation
|
|
142
|
+
|
|
143
|
+
Full documentation is available at: https://gperdrizet.github.io/CIFAR10/
|
|
144
|
+
|
|
145
|
+
## Demo project
|
|
146
|
+
|
|
147
|
+
See a complete example of using this package for CIFAR-10 classification:
|
|
148
|
+
https://github.com/gperdrizet/CIFAR10
|
|
149
|
+
|
|
150
|
+
## License
|
|
151
|
+
|
|
152
|
+
GPLv3
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""Image Classification Tools - General-purpose PyTorch image classification utilities.
|
|
2
|
+
|
|
3
|
+
This package provides tools for building, training, and evaluating image classification
|
|
4
|
+
models with PyTorch. It includes utilities for data loading, model training, evaluation,
|
|
5
|
+
hyperparameter optimization, and visualization.
|
|
6
|
+
"""
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'''PyTorch utilities for image classification.'''
|
|
2
|
+
|
|
3
|
+
from image_classification_tools.pytorch.data import (
|
|
4
|
+
load_dataset,
|
|
5
|
+
prepare_splits,
|
|
6
|
+
create_dataloaders,
|
|
7
|
+
generate_augmented_data
|
|
8
|
+
)
|
|
9
|
+
from image_classification_tools.pytorch.evaluation import evaluate_model
|
|
10
|
+
from image_classification_tools.pytorch.training import train_model
|
|
11
|
+
from image_classification_tools.pytorch.plotting import (
|
|
12
|
+
plot_sample_images,
|
|
13
|
+
plot_learning_curves,
|
|
14
|
+
plot_confusion_matrix,
|
|
15
|
+
plot_class_probability_distributions,
|
|
16
|
+
plot_evaluation_curves,
|
|
17
|
+
plot_optimization_results
|
|
18
|
+
)
|
|
19
|
+
from image_classification_tools.pytorch.hyperparameter_optimization import (
|
|
20
|
+
create_cnn,
|
|
21
|
+
train_trial,
|
|
22
|
+
create_objective
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
# Data loading and preprocessing
|
|
27
|
+
'load_dataset',
|
|
28
|
+
'prepare_splits',
|
|
29
|
+
'create_dataloaders',
|
|
30
|
+
'generate_augmented_data',
|
|
31
|
+
# Model evaluation
|
|
32
|
+
'evaluate_model',
|
|
33
|
+
# Model training
|
|
34
|
+
'train_model',
|
|
35
|
+
# Plotting and visualization
|
|
36
|
+
'plot_sample_images',
|
|
37
|
+
'plot_learning_curves',
|
|
38
|
+
'plot_confusion_matrix',
|
|
39
|
+
'plot_class_probability_distributions',
|
|
40
|
+
'plot_evaluation_curves',
|
|
41
|
+
'plot_optimization_results',
|
|
42
|
+
# Hyperparameter optimization
|
|
43
|
+
'create_cnn',
|
|
44
|
+
'train_trial',
|
|
45
|
+
'create_objective'
|
|
46
|
+
]
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
'''Data loading and preprocessing functions for image classification datasets.
|
|
2
|
+
|
|
3
|
+
This module provides utilities for loading datasets (including CIFAR-10) and creating
|
|
4
|
+
PyTorch DataLoaders with support for custom transforms and device preloading.
|
|
5
|
+
'''
|
|
6
|
+
|
|
7
|
+
# import os
|
|
8
|
+
import shutil
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Tuple
|
|
11
|
+
|
|
12
|
+
import torch
|
|
13
|
+
from torchvision import datasets, transforms
|
|
14
|
+
from torchvision.utils import save_image
|
|
15
|
+
from torch.utils.data import DataLoader, Dataset, TensorDataset, Subset
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def load_dataset(
|
|
19
|
+
data_source: Path | type,
|
|
20
|
+
transform: transforms.Compose,
|
|
21
|
+
train: bool = True,
|
|
22
|
+
download: bool = False,
|
|
23
|
+
**dataset_kwargs
|
|
24
|
+
) -> Dataset:
|
|
25
|
+
'''Load a single dataset from a directory or PyTorch dataset class.
|
|
26
|
+
|
|
27
|
+
This function provides a flexible interface for loading image classification datasets.
|
|
28
|
+
It supports both PyTorch built-in datasets (CIFAR-10, CIFAR-100, MNIST, etc.) and
|
|
29
|
+
custom datasets stored in directories following the ImageFolder structure.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
data_source: Either a Path to a directory containing train/ or test/ subdirectory,
|
|
33
|
+
or a PyTorch dataset class (e.g., datasets.CIFAR10)
|
|
34
|
+
transform: Transforms to apply to the data
|
|
35
|
+
train: If True, load training data. If False, load test data (default: True)
|
|
36
|
+
download: Whether to download the dataset if using a PyTorch dataset class.
|
|
37
|
+
Ignored for directory-based datasets.
|
|
38
|
+
**dataset_kwargs: Additional keyword arguments passed to the dataset class
|
|
39
|
+
(e.g., root='data/pytorch/cifar10')
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Dataset object
|
|
43
|
+
|
|
44
|
+
Examples:
|
|
45
|
+
# Load CIFAR-10 training data
|
|
46
|
+
train_dataset = load_dataset(
|
|
47
|
+
data_source=datasets.CIFAR10,
|
|
48
|
+
transform=transform,
|
|
49
|
+
train=True,
|
|
50
|
+
root='data/cifar10'
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Load from ImageFolder
|
|
54
|
+
train_dataset = load_dataset(
|
|
55
|
+
data_source=Path('data/my_dataset'),
|
|
56
|
+
transform=transform,
|
|
57
|
+
train=True
|
|
58
|
+
)
|
|
59
|
+
'''
|
|
60
|
+
|
|
61
|
+
if isinstance(data_source, Path):
|
|
62
|
+
# Directory-based dataset using ImageFolder
|
|
63
|
+
subdir = 'train' if train else 'test'
|
|
64
|
+
data_dir = data_source / subdir
|
|
65
|
+
|
|
66
|
+
if not data_dir.exists():
|
|
67
|
+
raise ValueError(f'{"Training" if train else "Test"} directory not found: {data_dir}')
|
|
68
|
+
|
|
69
|
+
return datasets.ImageFolder(
|
|
70
|
+
root=data_dir,
|
|
71
|
+
transform=transform
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
else:
|
|
75
|
+
# PyTorch dataset class (CIFAR-10, MNIST, etc.)
|
|
76
|
+
dataset_class = data_source
|
|
77
|
+
|
|
78
|
+
return dataset_class(
|
|
79
|
+
train=train,
|
|
80
|
+
download=download,
|
|
81
|
+
transform=transform,
|
|
82
|
+
**dataset_kwargs
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def prepare_splits(
|
|
87
|
+
train_dataset: Dataset,
|
|
88
|
+
test_dataset: Dataset | None = None,
|
|
89
|
+
val_size: int = 10000,
|
|
90
|
+
test_size: int | None = None
|
|
91
|
+
) -> Tuple[Dataset, Dataset, Dataset]:
|
|
92
|
+
'''Split training dataset into train/val(/test) splits.
|
|
93
|
+
|
|
94
|
+
The splitting behavior depends on whether a separate test dataset is provided:
|
|
95
|
+
- If test_dataset is provided: Split train_dataset into train/val only (2-way split)
|
|
96
|
+
- If test_dataset is None: Split train_dataset into train/val/test (3-way split)
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
train_dataset: Training dataset to split
|
|
100
|
+
test_dataset: Test dataset. If None, test set will be split from train_dataset.
|
|
101
|
+
val_size: Number of images to use for validation
|
|
102
|
+
test_size: Number of images to reserve for testing when test_dataset is None.
|
|
103
|
+
Only used when test_dataset is None. If None when test_dataset is None,
|
|
104
|
+
raises ValueError.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Tuple of (train_dataset, val_dataset, test_dataset)
|
|
108
|
+
|
|
109
|
+
Examples:
|
|
110
|
+
# 2-way split: Pass separate test set
|
|
111
|
+
train_ds, val_ds, test_ds = prepare_splits(
|
|
112
|
+
train_dataset=my_train_data,
|
|
113
|
+
test_dataset=my_test_data, # Use this for testing
|
|
114
|
+
val_size=10000 # 10,000 images for validation
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# 3-way split: No separate test set
|
|
118
|
+
train_ds, val_ds, test_ds = prepare_splits(
|
|
119
|
+
train_dataset=my_full_data,
|
|
120
|
+
test_dataset=None, # Will split test from train_dataset
|
|
121
|
+
val_size=10000, # 10,000 for validation
|
|
122
|
+
test_size=5000 # 5,000 for testing
|
|
123
|
+
)
|
|
124
|
+
'''
|
|
125
|
+
|
|
126
|
+
if test_dataset is not None:
|
|
127
|
+
|
|
128
|
+
# 2-way split: train/val only, use provided test set
|
|
129
|
+
total_size = len(train_dataset)
|
|
130
|
+
|
|
131
|
+
if val_size >= total_size:
|
|
132
|
+
raise ValueError(f'val_size ({val_size}) must be less than train_dataset size ({total_size})')
|
|
133
|
+
|
|
134
|
+
indices = torch.randperm(total_size).tolist()
|
|
135
|
+
|
|
136
|
+
val_indices = indices[:val_size]
|
|
137
|
+
train_indices = indices[val_size:]
|
|
138
|
+
|
|
139
|
+
train_dataset_final = Subset(train_dataset, train_indices)
|
|
140
|
+
val_dataset_final = Subset(train_dataset, val_indices)
|
|
141
|
+
test_dataset_final = test_dataset
|
|
142
|
+
|
|
143
|
+
else:
|
|
144
|
+
|
|
145
|
+
# 3-way split: train/val/test all from train_dataset
|
|
146
|
+
if test_size is None:
|
|
147
|
+
raise ValueError('test_size must be provided when test_dataset is None')
|
|
148
|
+
|
|
149
|
+
total_size = len(train_dataset)
|
|
150
|
+
|
|
151
|
+
if val_size + test_size >= total_size:
|
|
152
|
+
raise ValueError(
|
|
153
|
+
f'val_size ({val_size}) + test_size ({test_size}) must be less than '
|
|
154
|
+
f'train_dataset size ({total_size})'
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
indices = torch.randperm(total_size).tolist()
|
|
158
|
+
|
|
159
|
+
val_indices = indices[:val_size]
|
|
160
|
+
test_indices = indices[val_size:val_size + test_size]
|
|
161
|
+
train_indices = indices[val_size + test_size:]
|
|
162
|
+
|
|
163
|
+
train_dataset_final = Subset(train_dataset, train_indices)
|
|
164
|
+
val_dataset_final = Subset(train_dataset, val_indices)
|
|
165
|
+
test_dataset_final = Subset(train_dataset, test_indices)
|
|
166
|
+
|
|
167
|
+
return train_dataset_final, val_dataset_final, test_dataset_final
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def create_dataloaders(
|
|
171
|
+
train_dataset: Dataset,
|
|
172
|
+
val_dataset: Dataset,
|
|
173
|
+
test_dataset: Dataset,
|
|
174
|
+
batch_size: int,
|
|
175
|
+
shuffle_train: bool = True,
|
|
176
|
+
num_workers: int = 0,
|
|
177
|
+
preload_to_memory: bool = True,
|
|
178
|
+
device: torch.device | None = None,
|
|
179
|
+
**kwargs
|
|
180
|
+
) -> Tuple[DataLoader, DataLoader, DataLoader]:
|
|
181
|
+
'''Create DataLoaders from prepared datasets with optional memory preloading.
|
|
182
|
+
|
|
183
|
+
This function provides three memory management strategies:
|
|
184
|
+
1. Lazy loading (preload_to_memory=False): Data stays on disk, loaded per batch
|
|
185
|
+
2. CPU preloading (preload_to_memory=True, device=cpu): Entire dataset in RAM
|
|
186
|
+
3. GPU preloading (preload_to_memory=True, device=cuda): Entire dataset in VRAM
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
train_dataset: Prepared training dataset
|
|
190
|
+
val_dataset: Prepared validation dataset
|
|
191
|
+
test_dataset: Prepared test dataset
|
|
192
|
+
batch_size: Batch size for all DataLoaders
|
|
193
|
+
shuffle_train: Whether to shuffle training data (default: True)
|
|
194
|
+
num_workers: Number of subprocesses for data loading (default: 0 for single process).
|
|
195
|
+
Note: num_workers is ignored when preload_to_memory=True.
|
|
196
|
+
preload_to_memory: If True, convert datasets to tensors and load into memory.
|
|
197
|
+
If False, keep as lazy-loading Dataset objects (default: True).
|
|
198
|
+
device: Device to preload tensors onto. Only used if preload_to_memory=True.
|
|
199
|
+
If None with preload_to_memory=True, defaults to CPU.
|
|
200
|
+
Common values: torch.device('cpu'), torch.device('cuda')
|
|
201
|
+
**kwargs: Additional keyword arguments passed to DataLoader
|
|
202
|
+
(e.g., pin_memory=True, persistent_workers=True)
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
Tuple of (train_loader, val_loader, test_loader)
|
|
206
|
+
|
|
207
|
+
Examples:
|
|
208
|
+
# Strategy 1: Lazy loading (large datasets)
|
|
209
|
+
train_loader, val_loader, test_loader = create_dataloaders(
|
|
210
|
+
train_ds, val_ds, test_ds,
|
|
211
|
+
batch_size=128,
|
|
212
|
+
num_workers=4,
|
|
213
|
+
pin_memory=True
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
# Strategy 2: CPU preloading (medium datasets)
|
|
217
|
+
train_loader, val_loader, test_loader = create_dataloaders(
|
|
218
|
+
train_ds, val_ds, test_ds,
|
|
219
|
+
batch_size=128,
|
|
220
|
+
preload_to_memory=True,
|
|
221
|
+
device=torch.device('cpu')
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# Strategy 3: GPU preloading (small datasets, fastest training)
|
|
225
|
+
train_loader, val_loader, test_loader = create_dataloaders(
|
|
226
|
+
train_ds, val_ds, test_ds,
|
|
227
|
+
batch_size=128,
|
|
228
|
+
preload_to_memory=True,
|
|
229
|
+
device=torch.device('cuda')
|
|
230
|
+
)
|
|
231
|
+
'''
|
|
232
|
+
|
|
233
|
+
if preload_to_memory:
|
|
234
|
+
|
|
235
|
+
# Preload datasets to memory
|
|
236
|
+
if device is None:
|
|
237
|
+
device = torch.device('cpu')
|
|
238
|
+
|
|
239
|
+
# Load train data
|
|
240
|
+
X_train = torch.stack([img for img, _ in train_dataset]).to(device)
|
|
241
|
+
y_train = torch.tensor([label for _, label in train_dataset]).to(device)
|
|
242
|
+
train_dataset_final = TensorDataset(X_train, y_train)
|
|
243
|
+
|
|
244
|
+
# Load val data
|
|
245
|
+
X_val = torch.stack([img for img, _ in val_dataset]).to(device)
|
|
246
|
+
y_val = torch.tensor([label for _, label in val_dataset]).to(device)
|
|
247
|
+
val_dataset_final = TensorDataset(X_val, y_val)
|
|
248
|
+
|
|
249
|
+
# Load test data
|
|
250
|
+
X_test = torch.stack([img for img, _ in test_dataset]).to(device)
|
|
251
|
+
y_test = torch.tensor([label for _, label in test_dataset]).to(device)
|
|
252
|
+
test_dataset_final = TensorDataset(X_test, y_test)
|
|
253
|
+
|
|
254
|
+
# When preloading, num_workers should be 0
|
|
255
|
+
num_workers = 0
|
|
256
|
+
|
|
257
|
+
else:
|
|
258
|
+
|
|
259
|
+
# Use datasets as-is for lazy loading
|
|
260
|
+
train_dataset_final = train_dataset
|
|
261
|
+
val_dataset_final = val_dataset
|
|
262
|
+
test_dataset_final = test_dataset
|
|
263
|
+
|
|
264
|
+
train_loader = DataLoader(
|
|
265
|
+
train_dataset_final,
|
|
266
|
+
batch_size=batch_size,
|
|
267
|
+
shuffle=shuffle_train,
|
|
268
|
+
num_workers=num_workers,
|
|
269
|
+
**kwargs
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
val_loader = DataLoader(
|
|
273
|
+
val_dataset_final,
|
|
274
|
+
batch_size=batch_size,
|
|
275
|
+
shuffle=False,
|
|
276
|
+
num_workers=num_workers,
|
|
277
|
+
**kwargs
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
test_loader = DataLoader(
|
|
281
|
+
test_dataset_final,
|
|
282
|
+
batch_size=batch_size,
|
|
283
|
+
shuffle=False,
|
|
284
|
+
num_workers=num_workers,
|
|
285
|
+
**kwargs
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
return train_loader, val_loader, test_loader
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def generate_augmented_data(
|
|
292
|
+
train_dataset: Dataset,
|
|
293
|
+
augmentation_transforms: torch.nn.Sequential,
|
|
294
|
+
augmentations_per_image: int,
|
|
295
|
+
save_dir: str | Path,
|
|
296
|
+
class_names: list[str] | None = None,
|
|
297
|
+
chunk_size: int = 5000,
|
|
298
|
+
force_reaugment: bool = False
|
|
299
|
+
) -> None:
|
|
300
|
+
'''Generate augmented training data and save as ImageFolder-compatible directory structure.
|
|
301
|
+
|
|
302
|
+
This function applies augmentation transforms to create multiple augmented versions of each
|
|
303
|
+
training image and saves them to disk in ImageFolder format. Images are processed in chunks
|
|
304
|
+
to avoid memory issues with large datasets.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
train_dataset: PyTorch Dataset containing training images
|
|
308
|
+
augmentation_transforms: nn.Sequential containing augmentation transforms to apply
|
|
309
|
+
augmentations_per_image: Number of augmented versions to create per image
|
|
310
|
+
save_dir: Directory path to save augmented images in ImageFolder format
|
|
311
|
+
(will create class_0/, class_1/, etc. subdirectories)
|
|
312
|
+
class_names: Optional list of class names. If None, uses numeric class indices.
|
|
313
|
+
chunk_size: Number of images to process per chunk (default: 5000)
|
|
314
|
+
force_reaugment: If True, regenerate even if saved data exists
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
None (saves images to disk)
|
|
318
|
+
|
|
319
|
+
Example:
|
|
320
|
+
>>> generate_augmented_data(
|
|
321
|
+
... train_dataset=train_dataset,
|
|
322
|
+
... augmentation_transforms=augmentation_transforms,
|
|
323
|
+
... augmentations_per_image=3,
|
|
324
|
+
... save_dir='data/cifar10_augmented',
|
|
325
|
+
... class_names=['airplane', 'automobile', ...],
|
|
326
|
+
... chunk_size=5000
|
|
327
|
+
... )
|
|
328
|
+
>>> # Then load with existing pipeline:
|
|
329
|
+
>>> aug_dataset, _ = load_datasets(
|
|
330
|
+
... data_source=Path('data/cifar10_augmented'),
|
|
331
|
+
... transform=eval_transform
|
|
332
|
+
... )
|
|
333
|
+
'''
|
|
334
|
+
|
|
335
|
+
save_dir = Path(save_dir)
|
|
336
|
+
|
|
337
|
+
# Check if data already exists
|
|
338
|
+
if save_dir.exists() and any(save_dir.iterdir()) and not force_reaugment:
|
|
339
|
+
print(f'Augmented data already exists at {save_dir}')
|
|
340
|
+
print('Use force_reaugment=True to regenerate')
|
|
341
|
+
return
|
|
342
|
+
|
|
343
|
+
# Create directory structure with 'train' subdirectory for ImageFolder compatibility
|
|
344
|
+
if force_reaugment and save_dir.exists():
|
|
345
|
+
shutil.rmtree(save_dir)
|
|
346
|
+
|
|
347
|
+
save_dir.mkdir(parents=True, exist_ok=True)
|
|
348
|
+
train_dir = save_dir / 'train'
|
|
349
|
+
train_dir.mkdir(exist_ok=True)
|
|
350
|
+
|
|
351
|
+
# Get unique classes from dataset
|
|
352
|
+
print('Scanning dataset for classes...')
|
|
353
|
+
all_labels = set()
|
|
354
|
+
for _, label in train_dataset:
|
|
355
|
+
all_labels.add(label if isinstance(label, int) else label.item())
|
|
356
|
+
|
|
357
|
+
unique_classes = sorted(all_labels)
|
|
358
|
+
|
|
359
|
+
# Create class directories inside train/
|
|
360
|
+
for class_idx in unique_classes:
|
|
361
|
+
if class_names and class_idx < len(class_names):
|
|
362
|
+
class_dir = train_dir / class_names[class_idx]
|
|
363
|
+
else:
|
|
364
|
+
class_dir = train_dir / f'class_{class_idx}'
|
|
365
|
+
class_dir.mkdir(exist_ok=True)
|
|
366
|
+
|
|
367
|
+
print(f'Found {len(unique_classes)} classes')
|
|
368
|
+
print(f'Saving augmented images to {save_dir}')
|
|
369
|
+
|
|
370
|
+
original_size = len(train_dataset)
|
|
371
|
+
num_chunks = (original_size + chunk_size - 1) // chunk_size
|
|
372
|
+
|
|
373
|
+
print(f'Processing {original_size} images in {num_chunks} chunk(s)')
|
|
374
|
+
print(f'Generating {augmentations_per_image} augmentations per image')
|
|
375
|
+
|
|
376
|
+
total_saved = 0
|
|
377
|
+
|
|
378
|
+
# Process dataset in chunks
|
|
379
|
+
for chunk_idx in range(num_chunks):
|
|
380
|
+
start_idx = chunk_idx * chunk_size
|
|
381
|
+
end_idx = min((chunk_idx + 1) * chunk_size, original_size)
|
|
382
|
+
|
|
383
|
+
print(f'\nChunk {chunk_idx + 1}/{num_chunks} (images {start_idx}-{end_idx-1})...')
|
|
384
|
+
|
|
385
|
+
# Process each image in chunk
|
|
386
|
+
for idx in range(start_idx, end_idx):
|
|
387
|
+
img, label = train_dataset[idx]
|
|
388
|
+
label_val = label if isinstance(label, int) else label.item()
|
|
389
|
+
|
|
390
|
+
# Determine class directory (inside train/)
|
|
391
|
+
if class_names and label_val < len(class_names):
|
|
392
|
+
class_dir = train_dir / class_names[label_val]
|
|
393
|
+
else:
|
|
394
|
+
class_dir = train_dir / f'class_{label_val}'
|
|
395
|
+
|
|
396
|
+
# Save original image
|
|
397
|
+
img_name = f'img_{idx:06d}_orig.png'
|
|
398
|
+
save_image(img, class_dir / img_name)
|
|
399
|
+
total_saved += 1
|
|
400
|
+
|
|
401
|
+
# Generate and save augmented versions
|
|
402
|
+
for aug_idx in range(augmentations_per_image):
|
|
403
|
+
|
|
404
|
+
img_aug = augmentation_transforms(img.unsqueeze(0)).squeeze(0)
|
|
405
|
+
img_name = f'img_{idx:06d}_aug{aug_idx:02d}.png'
|
|
406
|
+
save_image(img_aug, class_dir / img_name)
|
|
407
|
+
total_saved += 1
|
|
408
|
+
|
|
409
|
+
print(f' Chunk {chunk_idx + 1} complete')
|
|
410
|
+
|
|
411
|
+
print(f'\nAugmentation complete!')
|
|
412
|
+
print(f' Total images saved: {total_saved}')
|
|
413
|
+
print(f' Original images: {original_size}')
|
|
414
|
+
print(f' Augmented images: {total_saved - original_size}')
|
|
415
|
+
print(f' Augmentation factor: {total_saved / original_size:.1f}x')
|
|
416
|
+
print(f' Location: {save_dir}')
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'''Evaluation functions for models.'''
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import torch
|
|
5
|
+
import torch.nn as nn
|
|
6
|
+
from torch.utils.data import DataLoader
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def evaluate_model(
|
|
10
|
+
model: nn.Module,
|
|
11
|
+
test_loader: DataLoader
|
|
12
|
+
) -> tuple[float, np.ndarray, np.ndarray]:
|
|
13
|
+
'''Evaluate model on test set.
|
|
14
|
+
|
|
15
|
+
Note: Assumes data is already on the correct device.
|
|
16
|
+
'''
|
|
17
|
+
|
|
18
|
+
model.eval()
|
|
19
|
+
correct = 0
|
|
20
|
+
total = 0
|
|
21
|
+
all_predictions = []
|
|
22
|
+
all_labels = []
|
|
23
|
+
|
|
24
|
+
with torch.no_grad():
|
|
25
|
+
|
|
26
|
+
for images, labels in test_loader:
|
|
27
|
+
|
|
28
|
+
outputs = model(images)
|
|
29
|
+
_, predicted = torch.max(outputs.data, 1)
|
|
30
|
+
|
|
31
|
+
total += labels.size(0)
|
|
32
|
+
correct += (predicted == labels).sum().item()
|
|
33
|
+
|
|
34
|
+
all_predictions.extend(predicted.cpu().numpy())
|
|
35
|
+
all_labels.extend(labels.cpu().numpy())
|
|
36
|
+
|
|
37
|
+
accuracy = 100 * correct / total
|
|
38
|
+
return accuracy, np.array(all_predictions), np.array(all_labels)
|