mnistvit 1.0.0__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.
- mnistvit/__init__.py +6 -0
- mnistvit/__main__.py +3 -0
- mnistvit/model.py +200 -0
- mnistvit/predict.py +244 -0
- mnistvit/preprocess.py +124 -0
- mnistvit/train.py +331 -0
- mnistvit/tune.py +240 -0
- mnistvit/utils.py +42 -0
- mnistvit-1.0.0.dist-info/LICENSE +674 -0
- mnistvit-1.0.0.dist-info/METADATA +752 -0
- mnistvit-1.0.0.dist-info/RECORD +13 -0
- mnistvit-1.0.0.dist-info/WHEEL +5 -0
- mnistvit-1.0.0.dist-info/top_level.txt +1 -0
mnistvit/__init__.py
ADDED
mnistvit/__main__.py
ADDED
mnistvit/model.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
from math import prod
|
|
2
|
+
from numbers import Number
|
|
3
|
+
from typing import Dict, List, Literal
|
|
4
|
+
|
|
5
|
+
from torch import Tensor, cat, nn, randn
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class VisionTransformer(nn.Module):
|
|
9
|
+
"""Configurable vision transformer (ViT).
|
|
10
|
+
|
|
11
|
+
Implements the vision transformer as proposed by Dosovitskiy et al., ICLR 2021.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
num_channels (int): Number of channels of the input.
|
|
15
|
+
input_sizes (list of int): Spatial sizes of the input.
|
|
16
|
+
output_size (int): Size of the output layer.
|
|
17
|
+
patch_size (int): Size of a patch in one dimension.
|
|
18
|
+
num_heads (int): Number of attention heads in each encoder block.
|
|
19
|
+
latent_size_multiplier (int): Yields the size of the embedding when multiplied
|
|
20
|
+
with `num_heads`.
|
|
21
|
+
num_layers (int): Number of encoder blocks.
|
|
22
|
+
encoder_size (int): Number of hidden units in each encoder MLP.
|
|
23
|
+
head_size (int or list of int): Sizes of hidden layers in MLP head.
|
|
24
|
+
dropout (float, optional): Dropout probabilities of embedding, encoder and MLP
|
|
25
|
+
head. Default: 0.
|
|
26
|
+
encoder_activation (str, optional): Encoder activation function string, either
|
|
27
|
+
`'relu'` or `'gelu'`. Default: `'gelu'`.
|
|
28
|
+
head_activation (str, optional): MLP head activation function string, `'relu'`,
|
|
29
|
+
`'gelu'` or `'tanh'`. Default: `'gelu'`.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
num_channels: int,
|
|
35
|
+
input_sizes: List[int],
|
|
36
|
+
output_size: int,
|
|
37
|
+
patch_size: int,
|
|
38
|
+
num_heads: int,
|
|
39
|
+
latent_size_multiplier: int,
|
|
40
|
+
num_layers: int,
|
|
41
|
+
encoder_size: int,
|
|
42
|
+
head_size: int | List[int],
|
|
43
|
+
dropout: float = 0,
|
|
44
|
+
encoder_activation: Literal["relu", "gelu"] = "gelu",
|
|
45
|
+
head_activation: Literal["relu", "gelu", "tanh"] = "gelu",
|
|
46
|
+
) -> None:
|
|
47
|
+
super().__init__()
|
|
48
|
+
self.kwargs = {
|
|
49
|
+
"num_channels": num_channels,
|
|
50
|
+
"input_sizes": input_sizes,
|
|
51
|
+
"output_size": output_size,
|
|
52
|
+
"patch_size": patch_size,
|
|
53
|
+
"num_heads": num_heads,
|
|
54
|
+
"latent_size_multiplier": latent_size_multiplier,
|
|
55
|
+
"num_layers": num_layers,
|
|
56
|
+
"encoder_size": encoder_size,
|
|
57
|
+
"head_size": head_size,
|
|
58
|
+
"dropout": dropout,
|
|
59
|
+
"encoder_activation": encoder_activation,
|
|
60
|
+
"head_activation": head_activation,
|
|
61
|
+
}
|
|
62
|
+
latent_size = latent_size_multiplier * num_heads
|
|
63
|
+
self.embedding = Embedding(
|
|
64
|
+
num_channels, input_sizes, patch_size, latent_size, dropout
|
|
65
|
+
)
|
|
66
|
+
layer_norm = nn.LayerNorm(latent_size)
|
|
67
|
+
encoder_layer = nn.TransformerEncoderLayer(
|
|
68
|
+
d_model=latent_size,
|
|
69
|
+
nhead=num_heads,
|
|
70
|
+
dim_feedforward=encoder_size,
|
|
71
|
+
dropout=dropout,
|
|
72
|
+
activation=encoder_activation,
|
|
73
|
+
batch_first=True,
|
|
74
|
+
norm_first=True,
|
|
75
|
+
)
|
|
76
|
+
self.encoder = nn.TransformerEncoder(
|
|
77
|
+
encoder_layer, num_layers, layer_norm, enable_nested_tensor=False
|
|
78
|
+
)
|
|
79
|
+
if isinstance(head_size, Number):
|
|
80
|
+
head_size = [head_size]
|
|
81
|
+
self.mlp_head = MLP(
|
|
82
|
+
input_size=latent_size,
|
|
83
|
+
output_size=output_size,
|
|
84
|
+
hidden_sizes=head_size,
|
|
85
|
+
dropout=dropout,
|
|
86
|
+
activation=head_activation,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def forward(self, data: Tensor) -> Tensor:
|
|
90
|
+
data = self.embedding(data)
|
|
91
|
+
data = self.encoder(data)
|
|
92
|
+
# Take encoder output corresponding to class_token
|
|
93
|
+
output = self.mlp_head(data[:, 0, :])
|
|
94
|
+
return output
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class Embedding(nn.Module):
|
|
98
|
+
"""An embedding for a vision transformer.
|
|
99
|
+
|
|
100
|
+
Splits the input into patches, and projects the patches. Also adds a class token
|
|
101
|
+
and a position embedding.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
num_channels (int): Number of channels of the input.
|
|
105
|
+
input_sizes (list of int): Spatial sizes of the input.
|
|
106
|
+
patch_size (int): Size of a patch in one dimension.
|
|
107
|
+
latent_size (int): Size of the embedding.
|
|
108
|
+
dropout (float, optional): Dropout probability. Default: 0.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
def __init__(
|
|
112
|
+
self,
|
|
113
|
+
num_channels: int,
|
|
114
|
+
input_sizes: List[int],
|
|
115
|
+
patch_size: int,
|
|
116
|
+
latent_size: int,
|
|
117
|
+
dropout: float = 0,
|
|
118
|
+
) -> None:
|
|
119
|
+
super().__init__()
|
|
120
|
+
# Use Unfold to split the input image into patches
|
|
121
|
+
self.unfold = nn.Unfold(kernel_size=patch_size, stride=patch_size)
|
|
122
|
+
num_patches = prod(
|
|
123
|
+
[(input_size - patch_size) // patch_size + 1 for input_size in input_sizes]
|
|
124
|
+
)
|
|
125
|
+
flattened_size = num_channels * patch_size ** len(input_sizes)
|
|
126
|
+
self.linear = nn.Linear(flattened_size, latent_size)
|
|
127
|
+
self.class_token = nn.Parameter(randn(1, 1, latent_size))
|
|
128
|
+
self.position_embeddings = nn.Parameter(randn(1, num_patches + 1, latent_size))
|
|
129
|
+
self.dropout = nn.Dropout(dropout)
|
|
130
|
+
|
|
131
|
+
def forward(self, data: Tensor) -> Tensor:
|
|
132
|
+
# Split images into patches and flatten into
|
|
133
|
+
# batch_size x num_patches x (num_channels * patch_size ** len(input_sizes))
|
|
134
|
+
batch_size = data.shape[0]
|
|
135
|
+
data = self.unfold(data).permute(0, 2, 1).contiguous()
|
|
136
|
+
data = self.linear(data)
|
|
137
|
+
output = self.dropout(
|
|
138
|
+
cat([self.class_token.expand(batch_size, -1, -1), data], dim=1)
|
|
139
|
+
+ self.position_embeddings
|
|
140
|
+
)
|
|
141
|
+
return output
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class MLP(nn.Module):
|
|
145
|
+
"""Configurable multilayer perceptron.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
input_size (int): Size of the input layer.
|
|
149
|
+
output_size (int): Size of the output layer.
|
|
150
|
+
hidden_sizes (list of int, optional): Sizes of hidden layers. Default: `None`.
|
|
151
|
+
dropout (float or list of float, optional): Dropout probabilities of each
|
|
152
|
+
hidden layer. If `None`, no dropout will be used. If single float, the
|
|
153
|
+
same dropout probability will be used for all hidden layers.
|
|
154
|
+
Default: `None`.
|
|
155
|
+
activation (str, optional): Activation function string, `'relu'`, `'gelu'` or
|
|
156
|
+
`'tanh'`. Default: `'relu'`.
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
def __init__(
|
|
160
|
+
self,
|
|
161
|
+
input_size: int,
|
|
162
|
+
output_size: int,
|
|
163
|
+
hidden_sizes: List[int] = None,
|
|
164
|
+
dropout: float | List[float] = None,
|
|
165
|
+
activation: Literal["relu", "gelu", "tanh"] = "relu",
|
|
166
|
+
) -> None:
|
|
167
|
+
super().__init__()
|
|
168
|
+
self.flatten = nn.Flatten()
|
|
169
|
+
if hidden_sizes is None:
|
|
170
|
+
hidden_sizes = []
|
|
171
|
+
layers = [
|
|
172
|
+
nn.Linear(i, j)
|
|
173
|
+
for i, j in zip([input_size] + hidden_sizes, hidden_sizes + [output_size])
|
|
174
|
+
]
|
|
175
|
+
if dropout is None:
|
|
176
|
+
dropout_modules = []
|
|
177
|
+
elif isinstance(dropout, Number):
|
|
178
|
+
dropout_modules = [nn.Dropout(dropout) for _ in range(len(hidden_sizes))]
|
|
179
|
+
else:
|
|
180
|
+
dropout_modules = [nn.Dropout(rate) for rate in dropout]
|
|
181
|
+
modules = []
|
|
182
|
+
for i, layer in enumerate(layers):
|
|
183
|
+
modules.append(layer)
|
|
184
|
+
if i < len(layers) - 1:
|
|
185
|
+
if activation == "relu":
|
|
186
|
+
modules.append(nn.ReLU())
|
|
187
|
+
elif activation == "gelu":
|
|
188
|
+
modules.append(nn.GELU())
|
|
189
|
+
elif activation == "tanh":
|
|
190
|
+
modules.append(nn.Tanh())
|
|
191
|
+
else:
|
|
192
|
+
raise ValueError(f"unknown activation '{activation}'")
|
|
193
|
+
if len(dropout_modules) > i:
|
|
194
|
+
modules.append(dropout_modules[i])
|
|
195
|
+
self.linear_stack = nn.Sequential(*modules)
|
|
196
|
+
|
|
197
|
+
def forward(self, data: Tensor) -> Tensor:
|
|
198
|
+
data = self.flatten(data)
|
|
199
|
+
output = self.linear_stack(data)
|
|
200
|
+
return output
|
mnistvit/predict.py
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from typing import Dict
|
|
3
|
+
|
|
4
|
+
import torch
|
|
5
|
+
|
|
6
|
+
from .preprocess import get_test_loader_mnist, read_digit_image
|
|
7
|
+
from .utils import FILE_LIKE, load_model
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_mnist(
|
|
11
|
+
config: Dict,
|
|
12
|
+
data_dir: str,
|
|
13
|
+
model_file: FILE_LIKE,
|
|
14
|
+
use_loss: bool = True,
|
|
15
|
+
use_accuracy: bool = True,
|
|
16
|
+
device: torch.device = "cpu",
|
|
17
|
+
) -> None:
|
|
18
|
+
"""Loads a model, tests it on MNIST and prints the results.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
config (dict): Test configuration with `'batch_size'`.
|
|
22
|
+
data_dir (str): Directory of the MNIST dataset.
|
|
23
|
+
model_file (FILE_LIKE): File name to load the model from.
|
|
24
|
+
use_loss (bool, optional): If true, evaluates the loss on the test set.
|
|
25
|
+
Default: `True`.
|
|
26
|
+
use_accuracy (bool, optional): If true, evaluates the accuracy on the test set.
|
|
27
|
+
Default: `True`.
|
|
28
|
+
device (torch.device, optional): Device to evaluate the model on.
|
|
29
|
+
Default: `'cpu'`.
|
|
30
|
+
"""
|
|
31
|
+
model = load_model(model_file, device)
|
|
32
|
+
test_loader = get_test_loader_mnist(data_dir, config["batch_size"])
|
|
33
|
+
if use_loss:
|
|
34
|
+
loss_fn = torch.nn.CrossEntropyLoss()
|
|
35
|
+
loss = prediction_loss(model, test_loader, loss_fn, device)
|
|
36
|
+
print("Test loss: ", loss)
|
|
37
|
+
if use_accuracy:
|
|
38
|
+
acc = prediction_accuracy(model, test_loader, device)
|
|
39
|
+
print("Test accuracy: ", acc)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def predict_file(
|
|
43
|
+
image_file: FILE_LIKE, model_file: FILE_LIKE, device: torch.device = "cpu"
|
|
44
|
+
) -> int:
|
|
45
|
+
"""Loads a model and classifies a digit from an image file.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
image_file (FILE_LIKE): The image file.
|
|
49
|
+
model_file (FILE_LIKE): File name to load the model from.
|
|
50
|
+
device (torch.device, optional): Device to evaluate the model on.
|
|
51
|
+
Default: `'cpu'`.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
int: Predicted class label.
|
|
55
|
+
"""
|
|
56
|
+
model = load_model(model_file, device)
|
|
57
|
+
image = read_digit_image(image_file)
|
|
58
|
+
predicted = predict_single_image(model, image, device)
|
|
59
|
+
return predicted
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def prediction_loss(
|
|
63
|
+
model: torch.nn.Module,
|
|
64
|
+
data_loader: torch.utils.data.DataLoader,
|
|
65
|
+
loss_fn: torch.nn.Module,
|
|
66
|
+
device: torch.device = "cpu",
|
|
67
|
+
) -> float:
|
|
68
|
+
"""Evaluates the model loss on the data loader.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
model (torch.nn.Module): Model to evaluate.
|
|
72
|
+
data_loader (torch.utils.data.DataLoader): Data loader for evaluation.
|
|
73
|
+
loss_fn (torch.nn.Module): Loss function for evaluation.
|
|
74
|
+
device (torch.device, optional): Device to evaluate the model on.
|
|
75
|
+
Default: `'cpu'`.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
float: Calculated loss.
|
|
79
|
+
"""
|
|
80
|
+
model.eval()
|
|
81
|
+
loss = 0
|
|
82
|
+
with torch.no_grad():
|
|
83
|
+
for data, target in data_loader:
|
|
84
|
+
data, target = data.to(device), target.to(device)
|
|
85
|
+
output = model(data)
|
|
86
|
+
loss += loss_fn(output, target).cpu().numpy()
|
|
87
|
+
loss /= len(data_loader)
|
|
88
|
+
return loss
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def prediction_accuracy(
|
|
92
|
+
model: torch.nn.Module,
|
|
93
|
+
data_loader: torch.utils.data.DataLoader,
|
|
94
|
+
device: torch.device = "cpu",
|
|
95
|
+
) -> float:
|
|
96
|
+
"""Evaluates the model accuracy on the data loader.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
model (torch.nn.Module): Model to evaluate.
|
|
100
|
+
data_loader (torch.utils.data.DataLoader): Data loader for evaluation.
|
|
101
|
+
device (torch.device, optional): Device to evaluate the model on.
|
|
102
|
+
Default: `'cpu'`.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
float: Calculated accuracy.
|
|
106
|
+
"""
|
|
107
|
+
correct = 0
|
|
108
|
+
total = 0
|
|
109
|
+
for data, target in data_loader:
|
|
110
|
+
data, target = data.to(device), target.to(device)
|
|
111
|
+
predicted = classify(model, data)
|
|
112
|
+
total += target.size(0)
|
|
113
|
+
correct += (predicted == target).sum().item()
|
|
114
|
+
return correct / total
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def predict_single_image(
|
|
118
|
+
model: torch.nn.Module, image: torch.FloatTensor, device: torch.device = "cpu"
|
|
119
|
+
) -> int:
|
|
120
|
+
"""Uses model to classify a digit image.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
model (torch.nn.Module): Model to use for classification.
|
|
124
|
+
image (torch.FloatTensor): Preprocessed digit image.
|
|
125
|
+
device (torch.device, optional): Device to evaluate the model on.
|
|
126
|
+
Default: `'cpu'`.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
int: Predicted class label.
|
|
130
|
+
"""
|
|
131
|
+
data = image.unsqueeze(0) # Add batch dimension
|
|
132
|
+
data = data.to(device)
|
|
133
|
+
predicted = classify(model, data).cpu().item()
|
|
134
|
+
return predicted
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def classify(model: torch.nn.Module, data: torch.utils.data.Sampler) -> torch.Tensor:
|
|
138
|
+
"""Uses model to classify given data.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
model (torch.nn.Module): Model to use for classification.
|
|
142
|
+
data (torch.utils.data.Sampler): Data sampler.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
torch.Tensor: Predicted class labels.
|
|
146
|
+
"""
|
|
147
|
+
output = eval_output(model, data)
|
|
148
|
+
predicted = torch.max(output.data, dim=1)[1]
|
|
149
|
+
return predicted
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def class_log_probs(
|
|
153
|
+
model: torch.nn.Module, data: torch.utils.data.Sampler
|
|
154
|
+
) -> torch.Tensor:
|
|
155
|
+
"""Evaluates model log probabilities of all classes on given data.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
model (torch.nn.Module): Model to use for evaluation.
|
|
159
|
+
data (torch.utils.data.Sampler): Data sampler.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
torch.Tensor: Log probabilities of classes.
|
|
163
|
+
"""
|
|
164
|
+
output = eval_output(model, data)
|
|
165
|
+
log_probs = torch.nn.functional.log_softmax(output.data, dim=1)
|
|
166
|
+
return log_probs
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def eval_output(model: torch.nn.Module, data: torch.utils.data.Sampler) -> torch.Tensor:
|
|
170
|
+
"""Evaluates the output of a model on given data.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
model (torch.nn.Module): Model to use for evaluation.
|
|
174
|
+
data (torch.utils.data.Sampler): Data sampler.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
torch.Tensor: Model output.
|
|
178
|
+
"""
|
|
179
|
+
model.eval()
|
|
180
|
+
with torch.no_grad():
|
|
181
|
+
output = model(data)
|
|
182
|
+
return output
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def main() -> None:
|
|
186
|
+
"""Processes command line arguments with prediction."""
|
|
187
|
+
parser = argparse.ArgumentParser(description="MNIST Prediction")
|
|
188
|
+
parser.add_argument(
|
|
189
|
+
"--image-file",
|
|
190
|
+
type=str,
|
|
191
|
+
default=None,
|
|
192
|
+
metavar="FILE",
|
|
193
|
+
help="image file to predict (default: None)",
|
|
194
|
+
)
|
|
195
|
+
parser.add_argument(
|
|
196
|
+
"--batch-size",
|
|
197
|
+
type=int,
|
|
198
|
+
default=32,
|
|
199
|
+
metavar="N",
|
|
200
|
+
help="input batch size for testing (default: 32)",
|
|
201
|
+
)
|
|
202
|
+
parser.add_argument(
|
|
203
|
+
"--model-file",
|
|
204
|
+
type=str,
|
|
205
|
+
default="model.pt",
|
|
206
|
+
metavar="FILE",
|
|
207
|
+
help="file to load the model from (default: 'model.pt')",
|
|
208
|
+
)
|
|
209
|
+
parser.add_argument(
|
|
210
|
+
"--use-loss",
|
|
211
|
+
action="store_true",
|
|
212
|
+
default=False,
|
|
213
|
+
help="enables test loss calculation",
|
|
214
|
+
)
|
|
215
|
+
parser.add_argument(
|
|
216
|
+
"--use-accuracy",
|
|
217
|
+
action="store_true",
|
|
218
|
+
default=False,
|
|
219
|
+
help="enables test accuracy calculation",
|
|
220
|
+
)
|
|
221
|
+
parser.add_argument(
|
|
222
|
+
"--no-cuda", action="store_true", default=False, help="disables CUDA testing"
|
|
223
|
+
)
|
|
224
|
+
args = parser.parse_args()
|
|
225
|
+
no_cuda = args.no_cuda or not torch.cuda.is_available()
|
|
226
|
+
device = torch.device("cpu" if no_cuda else "cuda")
|
|
227
|
+
config = {
|
|
228
|
+
"batch_size": args.batch_size,
|
|
229
|
+
}
|
|
230
|
+
if args.image_file is not None:
|
|
231
|
+
predicted = predict_file(args.image_file, args.model_file, device)
|
|
232
|
+
print(predicted)
|
|
233
|
+
test_mnist(
|
|
234
|
+
config,
|
|
235
|
+
data_dir="data",
|
|
236
|
+
model_file=args.model_file,
|
|
237
|
+
use_loss=args.use_loss,
|
|
238
|
+
use_accuracy=args.use_accuracy,
|
|
239
|
+
device=device,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
if __name__ == "__main__":
|
|
244
|
+
main()
|
mnistvit/preprocess.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
from typing import Tuple
|
|
2
|
+
|
|
3
|
+
import torch
|
|
4
|
+
from torch.utils.data import DataLoader, random_split
|
|
5
|
+
from torchvision import datasets, transforms
|
|
6
|
+
from torchvision.io import ImageReadMode, read_image
|
|
7
|
+
|
|
8
|
+
from .utils import FILE_LIKE
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def preprocess_mnist(
|
|
12
|
+
data_dir: str, train: bool, use_augmentation: bool = False
|
|
13
|
+
) -> Tuple:
|
|
14
|
+
"""Preprocesses the MNIST dataset.
|
|
15
|
+
|
|
16
|
+
Normalizes the MNIST dataset with training mean and variance and eventually
|
|
17
|
+
augments the dataset with random affine transformations.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
data_dir (str): Directory of the MNIST dataset.
|
|
21
|
+
train (bool): If true, loads the training set, else the test set.
|
|
22
|
+
use_augmentation (bool, optional): If true, augments the dataset with random
|
|
23
|
+
affine transformations. Default: `False`.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
tuple: Image, target pairs.
|
|
27
|
+
"""
|
|
28
|
+
transform = transforms.Compose(
|
|
29
|
+
[transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))]
|
|
30
|
+
)
|
|
31
|
+
if use_augmentation:
|
|
32
|
+
# Data augmentation
|
|
33
|
+
transform = transforms.Compose(
|
|
34
|
+
[
|
|
35
|
+
transforms.RandomAffine(
|
|
36
|
+
degrees=16, translate=(0.1, 0.1), scale=(0.9, 1.1)
|
|
37
|
+
),
|
|
38
|
+
transform,
|
|
39
|
+
]
|
|
40
|
+
)
|
|
41
|
+
dataset = datasets.MNIST(data_dir, train=train, download=True, transform=transform)
|
|
42
|
+
return dataset
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_train_loaders_mnist(
|
|
46
|
+
data_dir: str,
|
|
47
|
+
batch_size: int,
|
|
48
|
+
train_fraction: float = 1.0,
|
|
49
|
+
use_augmentation: bool = True,
|
|
50
|
+
) -> Tuple:
|
|
51
|
+
"""Training loaders of the MNIST dataset.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
data_dir (str): Directory of the MNIST dataset.
|
|
55
|
+
batch_size (int): Size of the batches of the training loaders.
|
|
56
|
+
train_fraction (float, optional): Fraction of the set used for the training
|
|
57
|
+
loader. The remainder is used for the validation loader. Default: 1.0.
|
|
58
|
+
use_augmentation (bool, optional): If true, augments the dataset with random
|
|
59
|
+
affine transformations. Default: `True`.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
tuple: Training loader and validation loader, where the latter is `None` if
|
|
63
|
+
`train_fraction` is 1.
|
|
64
|
+
"""
|
|
65
|
+
assert 0 <= train_fraction <= 1
|
|
66
|
+
loader_kwargs = {"shuffle": True, "batch_size": batch_size}
|
|
67
|
+
dataset = preprocess_mnist(data_dir, train=True, use_augmentation=use_augmentation)
|
|
68
|
+
if train_fraction == 1.0:
|
|
69
|
+
train_loader = DataLoader(dataset, **loader_kwargs)
|
|
70
|
+
val_loader = None
|
|
71
|
+
else:
|
|
72
|
+
num_train = int(len(dataset) * train_fraction)
|
|
73
|
+
train_set, val_set = random_split(
|
|
74
|
+
dataset, [num_train, len(dataset) - num_train]
|
|
75
|
+
)
|
|
76
|
+
train_loader = DataLoader(train_set, **loader_kwargs)
|
|
77
|
+
val_loader = DataLoader(val_set, **loader_kwargs)
|
|
78
|
+
return train_loader, val_loader
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def get_test_loader_mnist(
|
|
82
|
+
data_dir: str, batch_size: int
|
|
83
|
+
) -> torch.utils.data.DataLoader:
|
|
84
|
+
"""Test loader of the MNIST dataset.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
data_dir (str): Directory of the MNIST dataset.
|
|
88
|
+
batch_size (int): Size of the batches of the test loader.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
torch.utils.data.DataLoader: Test loader.
|
|
92
|
+
"""
|
|
93
|
+
dataset = preprocess_mnist(data_dir, train=False)
|
|
94
|
+
loader = DataLoader(dataset, shuffle=False, batch_size=batch_size)
|
|
95
|
+
return loader
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def read_digit_image(image_file: FILE_LIKE) -> torch.FloatTensor:
|
|
99
|
+
"""Loads a single digit image from a file.
|
|
100
|
+
|
|
101
|
+
Center crops and resizes the image to 28 by 28 pixels. Also inverts the image if
|
|
102
|
+
there are more bright than dark pixels.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
image_file (FILE_LIKE): The image file.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
torch.FloatTensor: The preprocessed digit image.
|
|
109
|
+
"""
|
|
110
|
+
image = read_image(image_file, mode=ImageReadMode.GRAY)
|
|
111
|
+
transform = transforms.Compose(
|
|
112
|
+
[
|
|
113
|
+
transforms.CenterCrop(min(image.size()[1:])),
|
|
114
|
+
transforms.Resize([28, 28]),
|
|
115
|
+
]
|
|
116
|
+
)
|
|
117
|
+
# Standardize image
|
|
118
|
+
image = transform(image).type(torch.FloatTensor)
|
|
119
|
+
image = transforms.Normalize(image.mean(), image.std())(image)
|
|
120
|
+
# Check if we need to invert the image
|
|
121
|
+
if (image > 0).count_nonzero() > image.numel() / 2:
|
|
122
|
+
# More bright than dark pixels
|
|
123
|
+
image = transforms.functional.invert(image)
|
|
124
|
+
return image
|