# ML Projects >[!Important] Welcome to my great list of working projects :smiley_cat: ## FasionMNIST ML with CNN :::info :information_source: Note This was a sample from the PyTorch Tutorial, QuickStart section; FNN model is replaced by CNN model for learning purposes. **Reflection**: I initially showed my boss a tutorial demo I put together that used the FasionMNIST dataset and a fully connected neural network (FNN). He recommended me try switching to a convolutional neural network (CNN), which is more appropriate for image-based tasks. This suggestion pushed me to dive into the architecture of CNNs-- the role of convolution and pooling layers and model definitions in PyTorch (see CNN notes above). Through this learning process, I gained familiarity with the how CNNs work and how it shapes the machine learning. ::: ```python! import torch import random from torch import nn from torch.utils.data import DataLoader from torchvision import datasets from torchvision.transforms import ToTensor import torch.nn.functional as F # Download training data from open datasets training_data = datasets.FashionMNIST( root="data", train=True, download=True, transform=ToTensor(), ) # Download test data from open datasets. test_data = datasets.FashionMNIST( root="data", train=False, download=True, transform=ToTensor(), ) # Batch size specifies how many samples per batch batch_size = 64 # Create data loaders train_dataloader = DataLoader(training_data, batch_size=batch_size) test_dataloader = DataLoader(test_data, batch_size=batch_size) for X, y in test_dataloader: print(f"Shape of X [N, C, H, W]: {X.shape}") print(f"Shape of y: {y.shape} {y.dtype}") break # Check for CUDA (GPU) availability and set device accordingly device = torch.device("cuda" if torch.cuda.is_available() else "cpu") print(f"Using {device} device") # Define the Simple CNN model class SimpleCNN(nn.Module): def __init__(self): super().__init__() # Define the convolutional layers self.conv1 = nn.Conv2d(1, 32, kernel_size=3, stride=1, padding=1) # 1 input channel (grayscale), 32 output channels self.conv2 = nn.Conv2d(32, 64, kernel_size=3, stride=1, padding=1) # 32 input channels, 64 output channels # Max pooling layer self.pool = nn.MaxPool2d(2, 2) # 2x2 max pooling # Fully connected layers self.fc1 = nn.Linear(64 * 7 * 7, 128) # After 2 pooling layers, image size is 7x7 with 64 channels self.fc2 = nn.Linear(128, 10) # 10 classes for output (Fashion-MNIST) def forward(self, x): # Apply the first convolutional layer followed by ReLU and pooling x = self.pool(F.relu(self.conv1(x))) # Apply the second convolutional layer followed by ReLU and pooling x = self.pool(F.relu(self.conv2(x))) # Flatten the output of the convolutional layers x = x.view(-1, 64 * 7 * 7) # Flatten the tensor # Apply the fully connected layers x = F.relu(self.fc1(x)) x = self.fc2(x) # Output layer return x # Create model and move it to the selected device (CPU or GPU) model = SimpleCNN().to(device) print(model) # Loss function and optimizer loss_fn = nn.CrossEntropyLoss() # Used for multi-class classification optimizer = torch.optim.SGD(model.parameters(), lr=1e-3) # Stochastic Gradient Descent (SGD) # Training function def train(dataloader, model, loss_fn, optimizer): size = len(dataloader.dataset) model.train() # Set model to training mode for batch, (X, y) in enumerate(dataloader): X, y = X.to(device), y.to(device) # Compute prediction error pred = model(X) loss = loss_fn(pred, y) # Backpropagation loss.backward() optimizer.step() optimizer.zero_grad() # Print every 100 batches if batch % 100 == 0: loss, current = loss.item(), (batch + 1) * len(X) print(f"loss: {loss:>7f} [{current:>5d}/{size:>5d}]") # Testing function def test(dataloader, model, loss_fn): size = len(dataloader.dataset) num_batches = len(dataloader) model.eval() # Set model to evaluation mode test_loss, correct = 0, 0 with torch.no_grad(): # Disable gradient tracking during testing for X, y in dataloader: X, y = X.to(device), y.to(device) pred = model(X) test_loss += loss_fn(pred, y).item() correct += (pred.argmax(1) == y).type(torch.float).sum().item() # argmax(1) selects the predicted class test_loss /= num_batches correct /= size print(f"Test Error: \n Accuracy: {(100 * correct):>0.1f}%, Avg loss: {test_loss:>8f} \n") # Train and test the model for a specified number of epochs epochs = 1 for t in range(epochs): print(f"Epoch {t+1}\n-------------------------------") train(train_dataloader, model, loss_fn, optimizer) test(test_dataloader, model, loss_fn) print("Done!") # Saving the model torch.save(model.state_dict(), "model.pth") print("Saved PyTorch Model State to model.pth") # Loading the model model = SimpleCNN().to(device) model.load_state_dict(torch.load("model.pth")) # Prediction classes = [ "T-shirt/top", "Trouser", "Pullover", "Dress", "Coat", "Sandal", "Shirt", "Sneaker", "Bag", "Ankle boot", ] # Test with a random image from the test set model.eval() index = random.randint(0, len(test_data) - 1) x, y = test_data[index][0], test_data[index][1] with torch.no_grad(): x = x.to(device) pred = model(x.unsqueeze(0)) # Add batch dimension predicted, actual = classes[pred[0].argmax(0)], classes[y] print(f'Predicted: "{predicted}", Actual: "{actual}"') ``` ## Stego Machine Learning :::info :information_source: Note Used stego database found on the internet, training model to scan for clean/non-clean images. Decorated with comments and very AI generated heavy for learning and checking understanding. **Reflection**: When I started this mini project, I thought I had a good idea of coding ML from scratch-- I quickly realized I didn't. Still, I decided to push out of my comfort zone to work with a database related to my passion, cybersecurity. Prior to this, I read an article about Steganography and learned a new aspect of cybersecurity I hadn't known before, inspiring me to build a simple model to detect malware in images. Using a dataset that wasn't already preloaded gave me more insight on how to load and use datasets. This mini project was a small step into deep waters as of my ML learning progress. While it stretched my current understanding of ML, it was also simple enough for me to comfortably guide me down my learning journey (with just two labels... stego & clean). ::: ```python= import torch import random from PIL import Image from torch import nn from torch.utils.data import DataLoader from torchvision import transforms, datasets from torchvision.transforms import ToTensor import torch.nn.functional as F #transforms images transform = transforms.Compose([ transforms.Grayscale(num_output_channels=1), transforms.Resize((28, 28)), #reduced significantly transforms.ToTensor(), ]) #ready dataset training_data = datasets.ImageFolder(root="data/train/train", transform=transform) test_data = datasets.ImageFolder(root="data/test/test", transform=transform) # Batch size specifies how many samples per batch batch_size = 128 # Create data loaders train_dataloader = DataLoader(training_data, batch_size=batch_size, shuffle = True) test_dataloader = DataLoader(test_data, batch_size=batch_size) for X, y in test_dataloader: print(f"Shape of X [N, C, H, W]: {X.shape}") print(f"Shape of y: {y.shape} {y.dtype}") break # Check for CUDA (GPU) availability and set device accordingly device = torch.device("cuda" if torch.cuda.is_available() else "cpu") print(f"Using {device} device...") # Define the Simple CNN model class SimpleCNN(nn.Module): def __init__(self): super().__init__() self.conv1 = nn.Conv2d(1, 32, 3, 1, 1) self.conv2 = nn.Conv2d(32, 64, 3, 1, 1) self.pool = nn.MaxPool2d(2, 2) self.fc1 = nn.Linear(64 * 7 * 7, 128) self.fc2 = nn.Linear(128, 1) def forward(self, x): x = self.pool(F.relu(self.conv1(x))) x = self.pool(F.relu(self.conv2(x))) x = x.view(-1, 64 * 7 * 7) x = F.relu(self.fc1(x)) return self.fc2(x) # Create model and move it to the selected device (CPU or GPU) model = SimpleCNN().to(device) print(model) # Loss function and optimizer loss_fn = nn.BCEWithLogitsLoss() optimizer = torch.optim.SGD(model.parameters(), lr=1e-3) # Stochastic Gradient Descent (SGD) # Training function def train(dataloader, model, loss_fn, optimizer): size = len(dataloader.dataset) model.train() for batch, (X, y) in enumerate(dataloader): X = X.to(device) labels = y.float().unsqueeze(1).to(device) # Convert to float and shape (B, 1) # Forward pass pred = model(X) loss = loss_fn(pred, labels) # Backpropagation loss.backward() optimizer.step() optimizer.zero_grad() if batch % 2 == 0: #reduced significantly current = batch * len(X) print(f"loss: {loss.item():>7f} [{current:>5d}/{size:>5d}]") # Testing function def test(dataloader, model, loss_fn): size = len(dataloader.dataset) num_batches = len(dataloader) test_loss, correct = 0, 0 model.eval() with torch.no_grad(): for X, y in dataloader: X = X.to(device) labels = y.float().unsqueeze(1).to(device) pred = model(X) probs = torch.sigmoid(pred) preds = (probs > 0.5).float() test_loss += loss_fn(pred, labels).item() correct += (preds == labels).sum().item() test_loss /= num_batches correct /= size print(f"Test Accuracy: {(100 * correct):.1f}%, Avg loss: {test_loss:.4f}") # Train and test the model for a specified number of epochs epochs = 1 #set to 1 for time for t in range(epochs): print(f"Epoch {t+1}\n-------------------------------") train(train_dataloader, model, loss_fn, optimizer) test(test_dataloader, model, loss_fn) print("Done!") # Saving the model torch.save(model.state_dict(), "model.pth") print("Saved PyTorch Model State to model.pth") # Loading the model model = SimpleCNN().to(device) model.load_state_dict(torch.load("model.pth")) # Test with a random image from the test set model.eval() index = random.randint(0, len(test_data) - 1) x, y = test_data[index] with torch.no_grad(): x = x.to(device) pred = model(x.unsqueeze(0)) prob = torch.sigmoid(pred).item() print(f'Predicted: {"Stego" if prob > 0.5 else "Clean"}, Actual: {"Stego" if y else "Clean"}') ``` <i class="fa fa-file-text"></i> [Stego-Images-Dataset](https://www.kaggle.com/datasets/marcozuppelli/stegoimagesdataset) >[time=Tues, Jun 10, 2025] ## Oxford Pet Machine Learning :::info :information_source: Note ML project on the Oxford Pet database focusing on code understanding with some experiments with visuals and matplotlib. Used PyTorch Tutorial, Learn the Basics, as a guide. **Reflections:** To maximize my learning and avoid line by line tutorial copying, I made the choice to follow the PyTorch Tutorial with with a different dataset: the Oxford IIIT Pet Dataset. With this guide, I was able to play with visuals and data transformations, tailoring my code to my dataset. It is also when I realized how fraustrating it is to train and work with ML without a GPU. I tried many ways to accommodate this, even scaling the image samples very tiny and sacraficing model accuracy. Through experimenting, I was able to grasp a deeper understanding on ML efficiency and speed. This miniproject forced me to apply my knowledge alongside learning new concepts about ML. ::: ```python= import os import torch import random import numpy as np import pandas as pd import torch.nn as nn import torch.optim as optim import matplotlib.pyplot as plt from collections import defaultdict from torchvision import datasets, transforms from torch.utils.data import Dataset, DataLoader from torchvision.io import decode_image, read_image from torchvision.transforms import ToTensor, Normalize, Resize #but wait! TRANSFORMS first! transform = transforms.Compose([ transforms.Resize((28,28)), transforms.RandomHorizontalFlip(), #randomly flips the image horizontally transforms.RandomRotation(10), #randomly rotates the image by 10 degrees transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.1), #randomly changes brightness, contrast, saturation, and hue transforms.ToTensor() ]) #LOADING DATASET training_data = datasets.OxfordIIITPet( root = "data", #path to where the data is stored split = "trainval", #specifies that it is training set download = True, #downloads dataset from the internet transform = transform ) test_data = datasets.OxfordIIITPet( root = "data", split = "test", download = True, transform = transform ) #VISUALIZING DATASET labels_map = { 0: "Abyssinian", 1: "American Bulldog", 2: "American Pit Bull Terrier", 3: "Basset Hound", 4: "Beagle", 5: "Bengal", 6: "Birman", 7: "Bombay", 8: "Boxer", 9: "British Shorthair", 10: "Chihuahua", 11: "Egyptian Mau", 12: "English Cocker Spaniel", 13: "English Setter", 14: "German Shorthaired Pointer", 15: "Great Pyrenees", 16: "Havanese", 17: "Japanese Chin", 18: "Keeshond", 19: "Leonberger", 20: "Maine Coon", 21: "Miniature Pinscher", 22: "Newfoundland", 23: "Persian", 24: "Pomeranian", 25: "Pug", 26: "Ragdoll", 27: "Russian Blue", 28: "Saint Bernard", 29: "Samoyed", 30: "Scottish Terrier", 31: "Shiba Inu", 32: "Siamese", 33: "Sphynx", 34: "Staffordshire Bull Terrier", 35: "Wheaten Terrier", 36: "Yorkshire Terrier", } num_classes = 37 #number of classes rows = 6 #6x7 grid cols = 7 figs, axes = plt.subplots(rows, cols, figsize = (cols*2, rows*2)) #scales the figure and sets each subplot to be about 2x2 #creates new Matplotlib Figure object #flatten axes for easier 1D access axes = axes.flatten() # Create a set to track which classes we've shown shown_classes = set() # Plot one example of each class for i in range(num_classes): # Keep trying until we find a class we haven't shown yet while True: #idx is randint from training data (1 element tensor shape) and returns as python int idx = torch.randint(len(training_data), (1,)).item() img, label = training_data[idx] # img: Tensor[C,H,W], label: integer class if label not in shown_classes: shown_classes.add(label) break # convert from Tensor[C,H,W] → NumPy[H,W,C] so matplotlib can display it # .permute(1,2,0) reorders dimensions: channel last # .numpy() turns it into a NumPy array img_np = img.permute(1,2,0).numpy() # Ensure values are between 0 and 1 for proper display img_np = np.clip(img_np, 0, 1) ax = axes[i] #select current plot ax.imshow(img_np) #shows the image ax.set_title(labels_map[label], fontsize = 8) #sets plot title ax.axis("off") #hides ticks and marks #turn off subplots not used for y in range(num_classes, len(axes)): axes[y].axis("off") #format layout plt.tight_layout() plt.show() #CREATING A CUSTOM DATASET (only if data is not convienently structured) class CustomImageDataset(Dataset): #initialize the directory containing the images, the annotations file, and both transforms #auto called when instance of database is instantiated def __init__(self,annotations_file, img_dir, transform=None, target_transform=None): #img_labels contains mappings of image names to labels self.img_labels = pd.read_csv(annotations_file) #Load your annotations CSV into memory self.img_dir = img_dir #remembers directory/folder path to raw images #store optional functions for preprocessing self.transform = transform #applied to imgs self.target_transform = target_transform #applies to labels #returns the number of samples in the dataset def __len__(self): return len(self.img_labels) #loads and returns img tensor and label corresponding to input idx def __getitem__(self, idx): #self.img_labels.iloc[idx, 0] looks up the first column of row idx in your annotations DataFrame—typically the filename #os.path.join(...) concatenates folder + filename img_path = os.path.join(self.img_dir, self.img_labels.iloc[idx,0] + ".jpg") image = read_image(img_path) #reads image into tensor label = self.img_labels.iloc[idx,1] - 1 #stores image int class id, usually in the second col if self.transform: image = self.transform(image) #apply image transforms if self.target_transform: label = self.target_transform(label) #apply target transforms return image,label #PREPARING DATA TO BE TRAINED WITH DATALOADERS #DataLoader organizes, batches, shuffles, and prefetches your data in a way that's both memory-safe and time-efficient train_dataloader = DataLoader(training_data, batch_size=64, shuffle = True) test_dataloader = DataLoader(test_data, batch_size = 64, shuffle =True) #ITERATING THROUGH DATALOADER #Display imgs and label #next() takes the first batch from that iterator #train_features is tensor of shape holding the images #train_labels is tensor of shape holding the labels train_features, train_labels = next(iter(train_dataloader)) #creates python iterator over training batches print(f"Feature batch shape: {train_features.size()}") #prints shape of features print(f"Labels batch shape: {train_labels.size()}") #prints shape of labels #grabs first sample and its label from that batch img_tensor = train_features[0] label = train_labels[0].item() print(f"Label value: {label}") #permute changes the dimin order so that it fits matplotlib expecting [height, width, channels] #.cpu() moves tensor to cpu because NumPy arrays need to be on cpu #.numpy() makes the tensor a numpy array :) img = img_tensor.permute(1,2,0).cpu().numpy() print("Displaying image...") plt.figure(figsize=(5,5)) plt.imshow(img) plt.title(f"Label: {label} Breed: {labels_map[label]}") plt.show() #BUILDING A NEURAL NETWORK #check accelerator availability device = torch.device("cuda" if torch.cuda.is_available() else "cpu") print(f"Using device: {device}") #Define CNN model #Define our neural network by subclassing nn.Module class cnn_model(nn.Module): #define the neural network layers in __init__ def __init__(self): super(cnn_model, self).__init__() #call the parent class constructor self.conv1 = nn.Conv2d(in_channels=3, out_channels=32, kernel_size=3, stride=1, padding=1) self.conv2 = nn.Conv2d(in_channels=32, out_channels=64, kernel_size=3, stride=2, padding=1) self.batch_norm1 = nn.BatchNorm2d(32) #batch norm allows for faster training and better performance; 64 is input bc conv2 output 64 self.batch_norm2 = nn.BatchNorm2d(64) #batch norm for conv2 output self.relu = nn.ReLU(inplace=True) #yay its non-linearity!; inplace saves memory self.max_pool = nn.MaxPool2d(kernel_size=2, stride=2) #pooling self.flatten = nn.Flatten() #flattens the output of the conv layers to a 1D vector self.fc1 = nn.Linear(in_features=576, out_features=128) #fully connected layer; 64*7*7 is the flattened size of the output from conv2 self.fc2 = nn.Linear(in_features=128, out_features=37) #the last layer must match the number of classes (37 for OxfordIIITPet) self.dropout = nn.Dropout(p=0.5) #dropout layer to prevent overfitting; 50% chance of dropping out a neuron #call these layers in desired order on x, the input tensor def forward(self, x): x = self.max_pool(self.relu(self.batch_norm1(self.conv1(x)))) x = self.max_pool(self.relu(self.batch_norm2(self.conv2(x)))) x = self.flatten(x) x = self.relu(self.fc1(x)) x = self.dropout(x) x = self.fc2(x) return x #create and move model to device model =cnn_model().to(device) print(model) #LOSS FUNCTION loss_fn = nn.CrossEntropyLoss() #cross entropy loss for multi-class classification #OPTIMIZER optimizer = optim.Adam(model.parameters(), lr=0.001, weight_decay= 1e-4) #Adam optimizer with learning rate of 0.001 #LR scheduler scheduler = torch.optim.lr_scheduler.CosineAnnealingLR( optimizer, T_max=10, #match epochs eta_min= 1e-6 # lower floor LR ) #TRAINING LOOP epochs = 10 for e in range(epochs): model.train() #set model to training mode running_loss = 0.0 #keeps track of total loss for this epoch correct = 0 total = 0 for images, labels in train_dataloader: images, labels = images.to(device), labels.to(device) #move data to device outputs = model(images) #forward pass; model predicts outputs loss = loss_fn(outputs, labels) #computes loss loss.backward() #backpropagation/autograd computes gradients optimizer.step() #weights are updated optimizer.zero_grad() #reset gradients to zero for next batch running_loss += loss.item() _, preds = torch.max(outputs,1) total += labels.size(0) correct +=(preds == labels).sum().item() scheduler.step() #update learning rate using scheduler accuracy = 100* correct / total print(f"Epoch [{e+1}/{epochs}], Loss: {loss.item():.4f}, Accuracy: {accuracy:.2f}%") #Testing the model def evaluate(model, dataloader): model.eval() #set model to evaluation mode with torch.no_grad(): correct = 0 total = 0 for images, labels in dataloader: images, labels = images.to(device), labels.to(device) outputs = model(images) preds = torch.argmax(outputs, dim=1) #get idx of highest score correct +=(preds == labels).sum().item() total += labels.size(0) accuracy = 100* correct / total print(f"Test Accuracy: {accuracy:.2f}%") for i in range(5): img = images[i].permute(1,2,0).cpu().numpy() plt.imshow(img) plt.title(f"Predicted: {labels_map[preds[i].item()]}, Actually: {labels_map[labels[i].item()]}") plt.axis("off") plt.show() evaluate(model, test_dataloader) #evaluate the model on the test set #Save model torch.save(model.state_dict(), "pet_cnn_model.pth") #Load model model = cnn_model().to(device) #create a new instance of the model model.load_state_dict(torch.load("pet_cnn_model.pth")) #load the saved state dict model.eval() #set model to evaluation mode ``` >[time=Thurs, Jun 12, 2025]