k23064919 commited on
Commit
deb385a
·
2 Parent(s): f622232 04cb886

integration between ops/clearml-setup and feature/ui-deplotment branches

Browse files
.gitignore CHANGED
@@ -1,5 +1,21 @@
 
1
  .vscode/
2
  .venv/
3
  .vscode/
4
  .models/
5
  __pycache__/
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <<<<<<< HEAD
2
  .vscode/
3
  .venv/
4
  .vscode/
5
  .models/
6
  __pycache__/
7
+ =======
8
+
9
+ # Python environment
10
+ venv/
11
+ *.pyc
12
+ __pycache__/
13
+
14
+ # Editor files
15
+ .DS_Store
16
+ .vscode/
17
+ .python-version
18
+
19
+ # Generated files from data_preparation.py
20
+ class_distribution.png
21
+ >>>>>>> 04cb88662062ef6b880c627546d067fa0cedfa8b
dataPrep/data_preparation.py ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # --- Standard Python Library ---
2
+ import os
3
+ import random
4
+
5
+ # --- Data Handling & Analysis ---
6
+ import numpy as np
7
+ import pandas as pd
8
+ from datasets import load_dataset
9
+ from helpers.create_dataset import load_subset_from_dataset
10
+ from helpers.transforms_loaders import make_dataset_loaders
11
+
12
+ # --- Visualization ---
13
+ import matplotlib.pyplot as plt
14
+ # import seaborn as sns
15
+
16
+ # --- PyTorch (Machine Learning) ---
17
+ import torch
18
+ from torchvision import transforms
19
+ from torch.utils.data import DataLoader
20
+
21
+ # --- Experiment Tracking ---
22
+ from clearml import Task, Logger, Dataset
23
+
24
+
25
+ # Setting up the SEED to be able to repeat experiments
26
+ SEED = 42
27
+ DATASET_SUBSET_RATIO = 0.25
28
+
29
+ random.seed(SEED)
30
+ np.random.seed(SEED)
31
+ torch.manual_seed(SEED)
32
+ if torch.cuda.is_available():
33
+ torch.cuda.manual_seed_all(SEED)
34
+
35
+
36
+ # ----- ClearML Setup -----
37
+ task = Task.init(project_name= 'Small Group CW', task_name = 'data_prep')
38
+ task.set_random_seed(SEED)
39
+ clearml_logger = task.get_logger()
40
+
41
+ # Log subset config to ClearML
42
+ task.connect_configuration(
43
+ {"subset_ratio": DATASET_SUBSET_RATIO},
44
+ name="Data subsetting"
45
+ )
46
+
47
+
48
+ # ----- Load a subset from a given dataset & track with ClearML -----
49
+ data_plants, prototyping_dataset, features, clearml_dataset = load_subset_from_dataset(
50
+ SEED, DATASET_SUBSET_RATIO, clearml_logger
51
+ )
52
+
53
+
54
+ # ---- Exploratory data analysis (EDA) ----
55
+
56
+ # Reformatting the label feature to understand bias
57
+ labels_list = prototyping_dataset['label']
58
+ df_labels = pd.Series(labels_list)
59
+ label_count = df_labels.value_counts(sort = False)
60
+
61
+ # Checking the amount of samples in each class and logging it to clearML
62
+
63
+ min_count = label_count.min()
64
+ clearml_logger.report_scalar(
65
+ title="Exploratory data analysis (EDA)",
66
+ series="Min Class Count",
67
+ value=min_count,
68
+ iteration=1
69
+ )
70
+
71
+ max_count = label_count.max()
72
+ clearml_logger.report_scalar(
73
+ title="Exploratory data analysis (EDA)",
74
+ series="Max Class Count",
75
+ value=max_count,
76
+ iteration=1
77
+ )
78
+
79
+ mean_count = label_count.mean()
80
+ clearml_logger.report_scalar(
81
+ title="Exploratory data analysis (EDA)",
82
+ series="Imbalance Ratio (Max/Min)",
83
+ value=(max_count / min_count),
84
+ iteration=1
85
+ )
86
+ print("--- Class imbalance analysis --- ")
87
+ print(f"Max labels in a class: {max_count}")
88
+ print(f"Min labels in a class: {min_count}")
89
+ print(f"Mean labels in a class: {mean_count}")
90
+ print(f"Imbalance ratio: {max_count/min_count:.2f}")
91
+
92
+ # Mapping indeces to class names
93
+ class_names = features['label'].names
94
+ formatted_class_names = [" ".join(name.replace('_', ' ').split()) for name in class_names]
95
+ label_count.index = formatted_class_names
96
+
97
+ plt.figure(figsize=(10,6))
98
+ label_count.plot(kind='bar', color='skyblue')
99
+ plt.title("Class Distribution in Prototype Dataset")
100
+ plt.xlabel("Class")
101
+ plt.ylabel("Count")
102
+ plt.tight_layout()
103
+ plt.savefig("class_distribution.png")
104
+
105
+ clearml_logger.report_image(
106
+ title="EDA Class Distribution",
107
+ series="Prototype Subset",
108
+ local_path="class_distribution.png",
109
+ iteration=1
110
+ )
111
+
112
+
113
+ # ----------------------------------------------------------------------
114
+ if __name__ == "__main__":
115
+
116
+ # ------------------- Dataset splits ----------------------------------
117
+ prototype_loaders = make_dataset_loaders(
118
+ prototyping_dataset, seed=SEED, batch_size=32, test_size=0.3
119
+ )
120
+
121
+ print("\n--- Handoff Test Successful ---")
122
+ print(f"Prototype Train loader batches: {len(prototype_loaders['train'])}")
123
+ print(f"Prototype Validation loader batches: {len(prototype_loaders['val'])}")
124
+ print(f"Prototype Test loader batches: {len(prototype_loaders['test'])}")
125
+
126
+ final_loaders = make_dataset_loaders(
127
+ data_plants, seed=SEED, batch_size=32, test_size=0.3
128
+ )
129
+
130
+ print("\n--- Handoff Test Successful ---")
131
+ print(f"Train loader batches: {len(final_loaders['train'])}")
132
+ print(f"Validation loader batches: {len(final_loaders['val'])}")
133
+ print(f"Test loader batches: {len(final_loaders['test'])}")
134
+
135
+ # Record dataset info in ClearML
136
+ task.connect_configuration(
137
+ {"dataset_id": clearml_dataset.id},
138
+ name="Dataset Metadata"
139
+ )
140
+
141
+ # Close the ClearML task
142
+ task.close()
143
+ print("\n--- Script Finished ---")
dataPrep/helpers/create_dataset.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ A collection of dataset (DS) loading and subsetting functions.
3
+ """
4
+
5
+ import random
6
+ import numpy as np
7
+ from datasets import load_dataset
8
+ from clearml import Dataset
9
+
10
+
11
+ # Load a DS from HuggingFace Link and subset - upload both to ClearML
12
+ def load_subset_from_dataset(seed, subset_ratio, clearml_logger):
13
+ DATASET_LINK = "DScomp380/plant_village"
14
+
15
+ # Load dataset
16
+ try:
17
+ ds = load_dataset(DATASET_LINK)
18
+ except Exception as e:
19
+ raise RuntimeError(f"Error loading the dataset: {e}")
20
+
21
+ data_plants = ds['train']
22
+ data_length = len(data_plants)
23
+ features = data_plants.features
24
+
25
+ # Calculate amount of samples we use
26
+ subset_size = int(data_length * subset_ratio)
27
+
28
+ # Creating a subset of random data (by their indicies)
29
+ indices = list(range(data_length))
30
+ random.shuffle(indices)
31
+ subset_indices = indices[:subset_size]
32
+
33
+ prototyping_dataset = data_plants.select(subset_indices)
34
+
35
+ # ---------- Register subset in ClearML ----------
36
+ clearml_dataset = Dataset.create(
37
+ dataset_name="Plant Village Prototype",
38
+ dataset_project="smallGroupProject",
39
+ dataset_tags=["prototype", "subset"]
40
+ )
41
+
42
+ # Save indices
43
+ subset_path = "subset_indices.npy"
44
+ np.save(subset_path, subset_indices)
45
+ clearml_dataset.add_files(subset_path)
46
+ clearml_dataset.set_metadata({
47
+ "subset_ratio": subset_ratio,
48
+ "total_samples": len(prototyping_dataset)
49
+ })
50
+
51
+ clearml_dataset.upload()
52
+ clearml_dataset.finalize()
53
+ clearml_logger.report_text(f"Created ClearML Dataset: {clearml_dataset.id}")
54
+
55
+ return data_plants, prototyping_dataset, features, clearml_dataset
dataPrep/helpers/transforms_loaders.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ A collection of data transformation and dataset loading functions.
3
+ """
4
+
5
+ from torchvision import transforms
6
+ from torch.utils.data import DataLoader
7
+
8
+
9
+
10
+ # Defines and returns the normalization and augmentation pipelines.
11
+ def make_transform_pipelines():
12
+
13
+ # Standard ImageNet mean and std - Used to normalize the tensors
14
+ IMAGENET_MEAN = [0.485, 0.456, 0.406]
15
+ IMAGENET_STD = [0.229, 0.224, 0.225]
16
+
17
+ # Pipeline ensures image format is consistent (for Val/Test)
18
+ normalisation = transforms.Compose([
19
+
20
+ # Convert PIL Image to a PyTorch Tensor, scales pixel values from [0, 255] to [0.0, 1.0]
21
+ transforms.ToTensor(),
22
+
23
+ # Standardises pixel values
24
+ transforms.Normalize(IMAGENET_MEAN, IMAGENET_STD)
25
+ ])
26
+
27
+ # Augmentation pipeline (to create "new" images by changing some parameters)
28
+ augmentation = transforms.Compose([
29
+
30
+ # Randomly changing some parameters of pictures to enrich dataset
31
+ transforms.RandomRotation(30),
32
+ transforms.ColorJitter(brightness=0.2, saturation=0.2),
33
+ transforms.GaussianBlur(3),
34
+ transforms.ToTensor(),
35
+ transforms.Normalize(IMAGENET_MEAN, IMAGENET_STD)
36
+ ])
37
+
38
+ return normalisation, augmentation
39
+
40
+
41
+ """
42
+ Creates and returns DataLoaders (train, val, test) for a given dataset.
43
+ Performs a 70/15/15 split
44
+ """
45
+ def make_dataset_loaders(dataset, seed, batch_size=32, test_size=0.3):
46
+
47
+ # Define transformation pipelines for the dataset
48
+ normalisation, augmentation = make_transform_pipelines()
49
+
50
+ # 70/30 split creates train set
51
+ split_1 = dataset.train_test_split(test_size=test_size, seed=seed)
52
+ train_split = split_1['train']
53
+ remaining_split = split_1['test']
54
+
55
+ # 15/15 split on remaining data - validation and test sets
56
+ val_split = test_size/2
57
+ split_2 = remaining_split.train_test_split(test_size=val_split, seed=seed)
58
+ val_split, test_split = split_2['train'], split_2['test']
59
+
60
+ # Put each split through pipelines
61
+ train_split.set_transform(augmentation)
62
+ val_split.set_transform(normalisation)
63
+ test_split.set_transform(normalisation)
64
+
65
+ # Create dataloader for each
66
+ train_loader = DataLoader(train_split, batch_size=batch_size, shuffle=True)
67
+ val_loader = DataLoader(val_split, batch_size=batch_size, shuffle=False)
68
+ test_loader = DataLoader(test_split, batch_size=batch_size, shuffle=False)
69
+
70
+ dataset_loaders = {
71
+ "train": train_loader,
72
+ "val": val_loader,
73
+ "test": test_loader
74
+ }
75
+
76
+ return dataset_loaders
models/modelOne.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ import torch.nn.functional as F
4
+
5
+ class modelOne(nn.Module) :
6
+ def __init__(self, noOfClasses=39):
7
+ super(modelOne, self).__init__()
8
+
9
+ self.conv1 = nn.Conv2d(3, 6, 5)
10
+ self.batchNorm1 = nn.BatchNorm2d(6)
11
+ self.pool = nn.MaxPool2d(2, 2)
12
+
13
+ self.conv2 = nn.Conv2d(6, 16, 5, padding=2)
14
+ self.batchNorm2 = nn.BatchNorm2d(16)
15
+
16
+ self.fc1 = nn.Linear(16*64*64, 512)
17
+ self.dropout = nn.Dropout(0.5)
18
+
19
+ self.fc2 = nn.Linear(512, 84)
20
+ self.fc3 = nn.Linear(84, noOfClasses)
21
+
22
+ def forward(self, x) :
23
+ x = self.pool(F.relu(self.batchNorm1(self.conv1(x))))
24
+ x = self.pool(F.relu(self.batchNorm2(self.conv2(x))))
25
+ x = torch.flatten(x, 1)
26
+ x = self.dropout(x)
27
+ x = F.relu(self.fc1(x))
28
+ x = F.relu(self.fc2(x))
29
+ x = self.fc3(x)
30
+
31
+ return x
requirements.txt CHANGED
@@ -1,3 +1,4 @@
 
1
  # Core dependencies
2
  torch>=2.0.0
3
  torchvision>=0.15.0
@@ -11,3 +12,20 @@ clearml>=1.14.0
11
 
12
  # Optional: for advanced features
13
  datasets>=2.14.0 # For loading PlantVillage dataset from HuggingFace
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <<<<<<< HEAD
2
  # Core dependencies
3
  torch>=2.0.0
4
  torchvision>=0.15.0
 
12
 
13
  # Optional: for advanced features
14
  datasets>=2.14.0 # For loading PlantVillage dataset from HuggingFace
15
+ =======
16
+ # -- Data prep requirements --
17
+ # Data Handling & Analysis
18
+ numpy
19
+ pandas
20
+ datasets
21
+
22
+ # Visualization
23
+ matplotlib
24
+
25
+ # PyTorch (Machine Learning)
26
+ torch
27
+ torchvision
28
+
29
+ # Experiment Tracking
30
+ clearml
31
+ >>>>>>> 04cb88662062ef6b880c627546d067fa0cedfa8b
trainingModel/Training.py ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ import numpy as np
4
+ from torcheval.metrics import MulticlassAccuracy
5
+ #from torchvision import transforms
6
+
7
+
8
+
9
+ from torch.utils.data import DataLoader
10
+ #from torchvision.datasets import MNIST
11
+
12
+ #import torchvision.utils
13
+
14
+ # loss, optimizer, training loop, validation, best model saving
15
+
16
+
17
+ def train_model(
18
+ model: nn.Module,
19
+ train_loader: DataLoader,
20
+ val_loader: DataLoader,
21
+ device: torch.device,
22
+ n_epochs: int = 4,
23
+ lr: float = 1e-3,
24
+ save_path: str = "best_model.pt",
25
+ flatten_input = False,
26
+ num_classes : int = 39,
27
+
28
+ ):
29
+
30
+
31
+
32
+ # Move model to device
33
+ model.to(device)
34
+
35
+ # Loss and optimizer
36
+ criterion = nn.CrossEntropyLoss()
37
+ optimizer = torch.optim.Adam(model.parameters(), lr=lr ) # might add momentum 0.9 later
38
+
39
+ # Metric trackers
40
+ train_accuracy_fn = MulticlassAccuracy(num_classes=num_classes)
41
+ val_accuracy_fn = MulticlassAccuracy(num_classes=num_classes)
42
+
43
+ # Arrays to log metrics
44
+ num_batches = len(train_loader)
45
+
46
+ # Store training losses and accuracies for every batch
47
+ # num_batches is the number of batches for every epoch
48
+ training_losses = np.zeros(num_batches * n_epochs)
49
+ training_accuracies = np.zeros(num_batches * n_epochs)
50
+
51
+
52
+ # store validation accuracy for every epoch
53
+ val_accuracies = np.zeros(n_epochs)
54
+ # keep track of best validation accuracy and best model
55
+ best_accuracy = 0.0
56
+
57
+
58
+
59
+ #----------------------
60
+ # training loop
61
+ #----------------------
62
+
63
+ for epoch in range(n_epochs):
64
+ model.train()
65
+ train_accuracy_fn.reset()
66
+
67
+ # iterate over all the dataloader's mini-batches
68
+ for i, batch in enumerate(train_loader):
69
+
70
+ # move to GPU memory
71
+ inputs = batch["image"].to(device)
72
+ labels = batch["label"].to(device)
73
+
74
+ # flatten if not cnn REVISE LATER
75
+ if flatten_input:
76
+ inputs = inputs.view(inputs.size(0), -1)
77
+
78
+
79
+ optimizer.zero_grad()
80
+
81
+
82
+ # Forward pass
83
+ outputs = model(inputs)
84
+ loss = criterion(outputs, labels)
85
+
86
+ # Backward pass
87
+ loss.backward()
88
+
89
+ # updates the parameters
90
+ optimizer.step()
91
+
92
+ # log the loss value
93
+ training_losses[epoch * num_batches + i] = loss.item()
94
+
95
+ # Compute accuracy of the batch.
96
+
97
+
98
+ #updates the accuracy computation with new data
99
+ train_accuracy_fn.update(outputs, labels)
100
+
101
+ #compute accuracy with the current data
102
+ training_accuracies[epoch * num_batches + i] = train_accuracy_fn.compute().item()
103
+
104
+
105
+ # display some progress (every 200 batches)
106
+ # optional, you can comment out
107
+ # if i % 200 == 0:
108
+ # print(f'Epoch {epoch + 1}, batch {i+1} of {len(train_loader)}')
109
+
110
+ print(f'Epoch {epoch + 1} training complete')
111
+
112
+ # Validation after each epoch
113
+ model.eval()
114
+ val_accuracy_fn.reset()
115
+
116
+
117
+ # The context 'torch.no_grad()' tells pytorch we are not interested in computing
118
+ # gradients here, so forward pass is more efficient
119
+ with torch.no_grad():
120
+ for i, batch in enumerate(val_loader):
121
+ inputs = batch["image"].to(device)
122
+ labels = batch["label"].to(device)
123
+
124
+ # flatten if not cnn REVISE LATER
125
+ if flatten_input:
126
+ inputs = inputs.view(inputs.size(0), -1)
127
+
128
+
129
+ outputs = model(inputs)
130
+
131
+ val_accuracy_fn.update(outputs, labels)
132
+
133
+ current_accuracy = val_accuracy_fn.compute().item()
134
+ val_accuracies[epoch] = current_accuracy
135
+
136
+
137
+ # keep track of best validation accuracy and save best model so far
138
+ if current_accuracy > best_accuracy:
139
+ best_accuracy = current_accuracy
140
+ torch.save(model.state_dict(), save_path)
141
+ print(f'Epoch {epoch + 1} (validation accuracy: {best_accuracy})')
142
+ print(f'Epoch {epoch + 1} validation complete')
143
+
144
+ print(f"\nTraining finished. Best val accuracy: {best_accuracy:.4f}")
145
+ print(f"Best model weights saved to: {save_path}")
146
+
147
+ return training_losses, training_accuracies, val_accuracies, best_accuracy
148
+
149
+
150
+ #tweak later
151
+ #best_model = MNISTNet().to(device)
152
+ #best_model.load_state_dict(
153
+ # torch.load('mnist-torch-best_model.pt', map_location=device))