# 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]