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 ADDED
@@ -0,0 +1,6 @@
1
+ """
2
+ `mnistvit` is a package providing a vision transformer for training on MNIST.
3
+
4
+ The package includes functions for training and evaluation, as well as hyperparameter
5
+ optimization.
6
+ """
mnistvit/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .predict import main
2
+
3
+ main()
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