Convolutional Neural Networks (CNN) for Soil Type Classification¶
Learning Objectives:
- Understand CNN architecture for image classification
- Learn data augmentation techniques for small datasets
- Build a CNN classifier in PyTorch
- Evaluate model performance with confusion matrix and metrics
Dataset: Soil type images organized in folders by class
- Black Soil
- Cinder Soil
- Laterite Soil
- Peat Soil
- Yellow Soil
1. Import Libraries¶
In [1]:
Copied!
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, Dataset, random_split
import torchvision
from torchvision import transforms
from torchvision.datasets import ImageFolder
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from PIL import Image
import os
from pathlib import Path
from tqdm.auto import tqdm
from sklearn.metrics import confusion_matrix, classification_report, accuracy_score
# Set random seeds
torch.manual_seed(42)
np.random.seed(42)
# Set device - prioritize MPS (Apple Silicon GPU)
if torch.backends.mps.is_available():
device = torch.device('mps')
print("🚀 Using Apple Silicon GPU (MPS) for acceleration!")
elif torch.cuda.is_available():
device = torch.device('cuda')
print("🚀 Using NVIDIA GPU (CUDA)")
else:
device = torch.device('cpu')
print("⚠️ Using CPU (this will be slow)")
print(f"Device: {device}")
print(f"PyTorch version: {torch.__version__}")
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, Dataset, random_split
import torchvision
from torchvision import transforms
from torchvision.datasets import ImageFolder
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from PIL import Image
import os
from pathlib import Path
from tqdm.auto import tqdm
from sklearn.metrics import confusion_matrix, classification_report, accuracy_score
# Set random seeds
torch.manual_seed(42)
np.random.seed(42)
# Set device - prioritize MPS (Apple Silicon GPU)
if torch.backends.mps.is_available():
device = torch.device('mps')
print("🚀 Using Apple Silicon GPU (MPS) for acceleration!")
elif torch.cuda.is_available():
device = torch.device('cuda')
print("🚀 Using NVIDIA GPU (CUDA)")
else:
device = torch.device('cpu')
print("⚠️ Using CPU (this will be slow)")
print(f"Device: {device}")
print(f"PyTorch version: {torch.__version__}")
🚀 Using Apple Silicon GPU (MPS) for acceleration! Device: mps PyTorch version: 2.9.0
/Users/krishna/courses/CE397-Scientific-MachineLearning/ai-geotech/env/lib/python3.11/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html from .autonotebook import tqdm as notebook_tqdm
In [14]:
Copied!
!wget https://github.com/kks32-courses/ai-geotech/raw/refs/heads/main/docs/03-cnn/Soil%20types.zip
!wget https://github.com/kks32-courses/ai-geotech/raw/refs/heads/main/docs/03-cnn/Soil%20types.zip
--2025-11-06 06:55:29-- https://github.com/kks32-courses/ai-geotech/raw/refs/heads/main/docs/03-cnn/Soil%20types.zip Resolving github.com (github.com)... 140.82.113.3 Connecting to github.com (github.com)|140.82.113.3|:443... connected. HTTP request sent, awaiting response... 302 Found Location: https://raw.githubusercontent.com/kks32-courses/ai-geotech/refs/heads/main/docs/03-cnn/Soil%20types.zip [following] --2025-11-06 06:55:29-- https://raw.githubusercontent.com/kks32-courses/ai-geotech/refs/heads/main/docs/03-cnn/Soil%20types.zip Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 185.199.108.133, 185.199.109.133, 185.199.110.133, ... Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|185.199.108.133|:443... connected. HTTP request sent, awaiting response... 200 OK Length: 3294489 (3.1M) [application/zip] Saving to: ‘Soil types.zip.1’ Soil types.zip.1 100%[===================>] 3.14M 19.9MB/s in 0.2s 2025-11-06 06:55:30 (19.9 MB/s) - ‘Soil types.zip.1’ saved [3294489/3294489]
In [ ]:
Copied!
!unzip Soil\ types.zip
!unzip Soil\ types.zip
2. Explore the Dataset¶
The dataset is organized as follows:
Soil types/
├── Black Soil/
│ ├── 10.jpg
│ ├── 11.jpg
│ └── ...
├── Cinder Soil/
├── Laterite Soil/
├── Peat Soil/
└── Yellow Soil/
In [2]:
Copied!
# Set data directory
data_dir = Path('Soil types')
# Get class names
class_names = sorted([d.name for d in data_dir.iterdir() if d.is_dir() and not d.name.startswith('.')])
print(f"📂 Found {len(class_names)} soil types:")
for i, class_name in enumerate(class_names):
num_images = len(list((data_dir / class_name).glob('*.jpg'))) + len(list((data_dir / class_name).glob('*.png')))
print(f" {i}. {class_name}: {num_images} images")
total_images = sum([len(list((data_dir / c).glob('*.jpg'))) + len(list((data_dir / c).glob('*.png'))) for c in class_names])
print(f"\n📊 Total images: {total_images}")
# Set data directory
data_dir = Path('Soil types')
# Get class names
class_names = sorted([d.name for d in data_dir.iterdir() if d.is_dir() and not d.name.startswith('.')])
print(f"📂 Found {len(class_names)} soil types:")
for i, class_name in enumerate(class_names):
num_images = len(list((data_dir / class_name).glob('*.jpg'))) + len(list((data_dir / class_name).glob('*.png')))
print(f" {i}. {class_name}: {num_images} images")
total_images = sum([len(list((data_dir / c).glob('*.jpg'))) + len(list((data_dir / c).glob('*.png'))) for c in class_names])
print(f"\n📊 Total images: {total_images}")
📂 Found 5 soil types: 0. Black Soil: 37 images 1. Cinder Soil: 30 images 2. Laterite Soil: 30 images 3. Peat Soil: 30 images 4. Yellow Soil: 29 images 📊 Total images: 156
3. Visualize Sample Images¶
In [3]:
Copied!
# Display sample images from each class
fig, axes = plt.subplots(1, len(class_names), figsize=(15, 3))
for i, class_name in enumerate(class_names):
# Get first image from class
img_path = list((data_dir / class_name).glob('*.jpg'))[0]
img = Image.open(img_path)
axes[i].imshow(img)
axes[i].set_title(class_name, fontsize=10)
axes[i].axis('off')
plt.tight_layout()
plt.show()
# Display sample images from each class
fig, axes = plt.subplots(1, len(class_names), figsize=(15, 3))
for i, class_name in enumerate(class_names):
# Get first image from class
img_path = list((data_dir / class_name).glob('*.jpg'))[0]
img = Image.open(img_path)
axes[i].imshow(img)
axes[i].set_title(class_name, fontsize=10)
axes[i].axis('off')
plt.tight_layout()
plt.show()
4. Data Augmentation and Preprocessing¶
Why Data Augmentation?
- Small dataset (~150 images total)
- Prevents overfitting
- Improves generalization
Transformations:
- Random horizontal/vertical flips
- Random rotation (±20°)
- Color jitter (brightness, contrast)
- Normalization
In [4]:
Copied!
# Image size
IMG_SIZE = 224
# Training transformations (with augmentation)
train_transform = transforms.Compose([
transforms.Resize((IMG_SIZE, IMG_SIZE)),
transforms.RandomHorizontalFlip(p=0.5),
transforms.RandomVerticalFlip(p=0.3),
transforms.RandomRotation(degrees=20),
transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.1),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
# Validation/Test transformations (no augmentation)
val_transform = transforms.Compose([
transforms.Resize((IMG_SIZE, IMG_SIZE)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
print("✅ Data augmentation pipeline created")
# Image size
IMG_SIZE = 224
# Training transformations (with augmentation)
train_transform = transforms.Compose([
transforms.Resize((IMG_SIZE, IMG_SIZE)),
transforms.RandomHorizontalFlip(p=0.5),
transforms.RandomVerticalFlip(p=0.3),
transforms.RandomRotation(degrees=20),
transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.1),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
# Validation/Test transformations (no augmentation)
val_transform = transforms.Compose([
transforms.Resize((IMG_SIZE, IMG_SIZE)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
print("✅ Data augmentation pipeline created")
✅ Data augmentation pipeline created
5. Create Dataset and DataLoaders¶
In [5]:
Copied!
# Load full dataset
print("📦 Loading dataset...")
full_dataset = ImageFolder(root=data_dir, transform=None)
# Split into train (70%), validation (15%), test (15%)
train_size = int(0.7 * len(full_dataset))
val_size = int(0.15 * len(full_dataset))
test_size = len(full_dataset) - train_size - val_size
print(f"\n📊 Dataset split:")
print(f" Train: {train_size} images ({train_size/len(full_dataset)*100:.1f}%)")
print(f" Val: {val_size} images ({val_size/len(full_dataset)*100:.1f}%)")
print(f" Test: {test_size} images ({test_size/len(full_dataset)*100:.1f}%)")
# Split dataset
train_dataset, val_dataset, test_dataset = random_split(
full_dataset, [train_size, val_size, test_size],
generator=torch.Generator().manual_seed(42)
)
# Apply transforms
train_dataset.dataset.transform = train_transform
val_dataset.dataset.transform = val_transform
test_dataset.dataset.transform = val_transform
# Create dataloaders
batch_size = 16
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, num_workers=0)
val_loader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False, num_workers=0)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False, num_workers=0)
print(f"\n✅ DataLoaders created (batch_size={batch_size})")
print(f" Train batches: {len(train_loader)}")
print(f" Val batches: {len(val_loader)}")
print(f" Test batches: {len(test_loader)}")
# Load full dataset
print("📦 Loading dataset...")
full_dataset = ImageFolder(root=data_dir, transform=None)
# Split into train (70%), validation (15%), test (15%)
train_size = int(0.7 * len(full_dataset))
val_size = int(0.15 * len(full_dataset))
test_size = len(full_dataset) - train_size - val_size
print(f"\n📊 Dataset split:")
print(f" Train: {train_size} images ({train_size/len(full_dataset)*100:.1f}%)")
print(f" Val: {val_size} images ({val_size/len(full_dataset)*100:.1f}%)")
print(f" Test: {test_size} images ({test_size/len(full_dataset)*100:.1f}%)")
# Split dataset
train_dataset, val_dataset, test_dataset = random_split(
full_dataset, [train_size, val_size, test_size],
generator=torch.Generator().manual_seed(42)
)
# Apply transforms
train_dataset.dataset.transform = train_transform
val_dataset.dataset.transform = val_transform
test_dataset.dataset.transform = val_transform
# Create dataloaders
batch_size = 16
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, num_workers=0)
val_loader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False, num_workers=0)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False, num_workers=0)
print(f"\n✅ DataLoaders created (batch_size={batch_size})")
print(f" Train batches: {len(train_loader)}")
print(f" Val batches: {len(val_loader)}")
print(f" Test batches: {len(test_loader)}")
📦 Loading dataset... 📊 Dataset split: Train: 109 images (69.9%) Val: 23 images (14.7%) Test: 24 images (15.4%) ✅ DataLoaders created (batch_size=16) Train batches: 7 Val batches: 2 Test batches: 2
6. CNN Model Architecture¶
Our CNN consists of:
- 3 Convolutional blocks (Conv → ReLU → MaxPool)
- Dropout for regularization
- 2 Fully connected layers
- Output layer (5 classes)
Input (224x224x3)
↓
Conv Block 1 (32 filters, 3x3)
↓ MaxPool (112x112)
Conv Block 2 (64 filters, 3x3)
↓ MaxPool (56x56)
Conv Block 3 (128 filters, 3x3)
↓ MaxPool (28x28)
Flatten
↓
FC1 (256 units) + Dropout
↓
FC2 (128 units) + Dropout
↓
Output (5 classes)
In [6]:
Copied!
class SoilCNN(nn.Module):
def __init__(self, num_classes=5):
super(SoilCNN, self).__init__()
# Convolutional Block 1
self.conv1 = nn.Sequential(
nn.Conv2d(3, 32, kernel_size=3, padding=1),
nn.BatchNorm2d(32),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2)
)
# Convolutional Block 2
self.conv2 = nn.Sequential(
nn.Conv2d(32, 64, kernel_size=3, padding=1),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2)
)
# Convolutional Block 3
self.conv3 = nn.Sequential(
nn.Conv2d(64, 128, kernel_size=3, padding=1),
nn.BatchNorm2d(128),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2)
)
# Calculate flattened size: 224 -> 112 -> 56 -> 28
self.flatten_size = 128 * 28 * 28
# Fully connected layers
self.fc = nn.Sequential(
nn.Flatten(),
nn.Linear(self.flatten_size, 256),
nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(256, 128),
nn.ReLU(),
nn.Dropout(0.3),
nn.Linear(128, num_classes)
)
def forward(self, x):
x = self.conv1(x)
x = self.conv2(x)
x = self.conv3(x)
x = self.fc(x)
return x
# Create model
model = SoilCNN(num_classes=len(class_names))
model = model.to(device)
print(model)
print(f"\n📊 Total parameters: {sum(p.numel() for p in model.parameters()):,}")
print(f"📊 Trainable parameters: {sum(p.numel() for p in model.parameters() if p.requires_grad):,}")
class SoilCNN(nn.Module):
def __init__(self, num_classes=5):
super(SoilCNN, self).__init__()
# Convolutional Block 1
self.conv1 = nn.Sequential(
nn.Conv2d(3, 32, kernel_size=3, padding=1),
nn.BatchNorm2d(32),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2)
)
# Convolutional Block 2
self.conv2 = nn.Sequential(
nn.Conv2d(32, 64, kernel_size=3, padding=1),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2)
)
# Convolutional Block 3
self.conv3 = nn.Sequential(
nn.Conv2d(64, 128, kernel_size=3, padding=1),
nn.BatchNorm2d(128),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2)
)
# Calculate flattened size: 224 -> 112 -> 56 -> 28
self.flatten_size = 128 * 28 * 28
# Fully connected layers
self.fc = nn.Sequential(
nn.Flatten(),
nn.Linear(self.flatten_size, 256),
nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(256, 128),
nn.ReLU(),
nn.Dropout(0.3),
nn.Linear(128, num_classes)
)
def forward(self, x):
x = self.conv1(x)
x = self.conv2(x)
x = self.conv3(x)
x = self.fc(x)
return x
# Create model
model = SoilCNN(num_classes=len(class_names))
model = model.to(device)
print(model)
print(f"\n📊 Total parameters: {sum(p.numel() for p in model.parameters()):,}")
print(f"📊 Trainable parameters: {sum(p.numel() for p in model.parameters() if p.requires_grad):,}")
SoilCNN(
(conv1): Sequential(
(0): Conv2d(3, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(1): BatchNorm2d(32, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(2): ReLU()
(3): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
)
(conv2): Sequential(
(0): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(2): ReLU()
(3): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
)
(conv3): Sequential(
(0): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(2): ReLU()
(3): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
)
(fc): Sequential(
(0): Flatten(start_dim=1, end_dim=-1)
(1): Linear(in_features=100352, out_features=256, bias=True)
(2): ReLU()
(3): Dropout(p=0.5, inplace=False)
(4): Linear(in_features=256, out_features=128, bias=True)
(5): ReLU()
(6): Dropout(p=0.3, inplace=False)
(7): Linear(in_features=128, out_features=5, bias=True)
)
)
📊 Total parameters: 25,817,605
📊 Trainable parameters: 25,817,605
7. Training Configuration¶
In [7]:
Copied!
# Loss function and optimizer
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
# Learning rate scheduler (reduce LR on plateau)
scheduler = optim.lr_scheduler.ReduceLROnPlateau(
optimizer, mode='min', factor=0.5, patience=5
)
print("✅ Training configuration:")
print(f" Loss: CrossEntropyLoss")
print(f" Optimizer: Adam (lr=0.001)")
print(f" Scheduler: ReduceLROnPlateau")
# Loss function and optimizer
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
# Learning rate scheduler (reduce LR on plateau)
scheduler = optim.lr_scheduler.ReduceLROnPlateau(
optimizer, mode='min', factor=0.5, patience=5
)
print("✅ Training configuration:")
print(f" Loss: CrossEntropyLoss")
print(f" Optimizer: Adam (lr=0.001)")
print(f" Scheduler: ReduceLROnPlateau")
✅ Training configuration: Loss: CrossEntropyLoss Optimizer: Adam (lr=0.001) Scheduler: ReduceLROnPlateau
8. Training Function with Progress Bar¶
In [8]:
Copied!
def train_model(model, train_loader, val_loader, criterion, optimizer, scheduler,
num_epochs=50, device='cpu'):
"""
Train the CNN model with real-time progress tracking.
"""
history = {
'train_loss': [],
'train_acc': [],
'val_loss': [],
'val_acc': []
}
best_val_acc = 0.0
print(f"🏃 Starting training on {device}...\n")
for epoch in tqdm(range(num_epochs), desc='Epochs'):
# Training phase
model.train()
train_loss = 0.0
train_correct = 0
train_total = 0
for images, labels in train_loader:
images, labels = images.to(device), labels.to(device)
optimizer.zero_grad()
outputs = model(images)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
train_loss += loss.item()
_, predicted = torch.max(outputs.data, 1)
train_total += labels.size(0)
train_correct += (predicted == labels).sum().item()
train_loss /= len(train_loader)
train_acc = 100 * train_correct / train_total
# Validation phase
model.eval()
val_loss = 0.0
val_correct = 0
val_total = 0
with torch.no_grad():
for images, labels in val_loader:
images, labels = images.to(device), labels.to(device)
outputs = model(images)
loss = criterion(outputs, labels)
val_loss += loss.item()
_, predicted = torch.max(outputs.data, 1)
val_total += labels.size(0)
val_correct += (predicted == labels).sum().item()
val_loss /= len(val_loader)
val_acc = 100 * val_correct / val_total
# Update history
history['train_loss'].append(train_loss)
history['train_acc'].append(train_acc)
history['val_loss'].append(val_loss)
history['val_acc'].append(val_acc)
# Update learning rate
scheduler.step(val_loss)
# Save best model
if val_acc > best_val_acc:
best_val_acc = val_acc
torch.save(model.state_dict(), 'best_soil_cnn.pth')
# Print progress every 10 epochs
if (epoch + 1) % 10 == 0:
print(f'Epoch {epoch+1}/{num_epochs} | '
f'Train Loss: {train_loss:.4f}, Acc: {train_acc:.2f}% | '
f'Val Loss: {val_loss:.4f}, Acc: {val_acc:.2f}%')
print(f'\n✅ Training completed! Best validation accuracy: {best_val_acc:.2f}%')
# Load best model
model.load_state_dict(torch.load('best_soil_cnn.pth'))
return history
def train_model(model, train_loader, val_loader, criterion, optimizer, scheduler,
num_epochs=50, device='cpu'):
"""
Train the CNN model with real-time progress tracking.
"""
history = {
'train_loss': [],
'train_acc': [],
'val_loss': [],
'val_acc': []
}
best_val_acc = 0.0
print(f"🏃 Starting training on {device}...\n")
for epoch in tqdm(range(num_epochs), desc='Epochs'):
# Training phase
model.train()
train_loss = 0.0
train_correct = 0
train_total = 0
for images, labels in train_loader:
images, labels = images.to(device), labels.to(device)
optimizer.zero_grad()
outputs = model(images)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
train_loss += loss.item()
_, predicted = torch.max(outputs.data, 1)
train_total += labels.size(0)
train_correct += (predicted == labels).sum().item()
train_loss /= len(train_loader)
train_acc = 100 * train_correct / train_total
# Validation phase
model.eval()
val_loss = 0.0
val_correct = 0
val_total = 0
with torch.no_grad():
for images, labels in val_loader:
images, labels = images.to(device), labels.to(device)
outputs = model(images)
loss = criterion(outputs, labels)
val_loss += loss.item()
_, predicted = torch.max(outputs.data, 1)
val_total += labels.size(0)
val_correct += (predicted == labels).sum().item()
val_loss /= len(val_loader)
val_acc = 100 * val_correct / val_total
# Update history
history['train_loss'].append(train_loss)
history['train_acc'].append(train_acc)
history['val_loss'].append(val_loss)
history['val_acc'].append(val_acc)
# Update learning rate
scheduler.step(val_loss)
# Save best model
if val_acc > best_val_acc:
best_val_acc = val_acc
torch.save(model.state_dict(), 'best_soil_cnn.pth')
# Print progress every 10 epochs
if (epoch + 1) % 10 == 0:
print(f'Epoch {epoch+1}/{num_epochs} | '
f'Train Loss: {train_loss:.4f}, Acc: {train_acc:.2f}% | '
f'Val Loss: {val_loss:.4f}, Acc: {val_acc:.2f}%')
print(f'\n✅ Training completed! Best validation accuracy: {best_val_acc:.2f}%')
# Load best model
model.load_state_dict(torch.load('best_soil_cnn.pth'))
return history
9. Train the Model¶
In [9]:
Copied!
# Train the model
history = train_model(
model=model,
train_loader=train_loader,
val_loader=val_loader,
criterion=criterion,
optimizer=optimizer,
scheduler=scheduler,
num_epochs=50,
device=device
)
# Train the model
history = train_model(
model=model,
train_loader=train_loader,
val_loader=val_loader,
criterion=criterion,
optimizer=optimizer,
scheduler=scheduler,
num_epochs=50,
device=device
)
🏃 Starting training on mps...
Epochs: 20%|██ | 10/50 [00:09<00:17, 2.28it/s]
Epoch 10/50 | Train Loss: 0.9457, Acc: 74.31% | Val Loss: 0.4654, Acc: 78.26%
Epochs: 40%|████ | 20/50 [00:12<00:11, 2.63it/s]
Epoch 20/50 | Train Loss: 0.4945, Acc: 86.24% | Val Loss: 0.6874, Acc: 73.91%
Epochs: 60%|██████ | 30/50 [00:16<00:06, 2.92it/s]
Epoch 30/50 | Train Loss: 0.2818, Acc: 88.99% | Val Loss: 0.7239, Acc: 69.57%
Epochs: 80%|████████ | 40/50 [00:19<00:03, 2.95it/s]
Epoch 40/50 | Train Loss: 0.2693, Acc: 88.07% | Val Loss: 0.7231, Acc: 69.57%
Epochs: 100%|██████████| 50/50 [00:23<00:00, 2.15it/s]
Epoch 50/50 | Train Loss: 0.2175, Acc: 93.58% | Val Loss: 0.7370, Acc: 73.91% ✅ Training completed! Best validation accuracy: 82.61%
10. Visualize Training History¶
In [10]:
Copied!
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# Plot loss
axes[0].plot(history['train_loss'], 'b-', label='Train Loss', linewidth=2)
axes[0].plot(history['val_loss'], 'r-', label='Val Loss', linewidth=2)
axes[0].set_xlabel('Epoch', fontsize=12)
axes[0].set_ylabel('Loss', fontsize=12)
axes[0].set_title('Training and Validation Loss', fontsize=14)
axes[0].legend()
axes[0].grid(True, alpha=0.3)
# Plot accuracy
axes[1].plot(history['train_acc'], 'b-', label='Train Accuracy', linewidth=2)
axes[1].plot(history['val_acc'], 'r-', label='Val Accuracy', linewidth=2)
axes[1].set_xlabel('Epoch', fontsize=12)
axes[1].set_ylabel('Accuracy (%)', fontsize=12)
axes[1].set_title('Training and Validation Accuracy', fontsize=14)
axes[1].legend()
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
print(f"Final train accuracy: {history['train_acc'][-1]:.2f}%")
print(f"Final val accuracy: {history['val_acc'][-1]:.2f}%")
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# Plot loss
axes[0].plot(history['train_loss'], 'b-', label='Train Loss', linewidth=2)
axes[0].plot(history['val_loss'], 'r-', label='Val Loss', linewidth=2)
axes[0].set_xlabel('Epoch', fontsize=12)
axes[0].set_ylabel('Loss', fontsize=12)
axes[0].set_title('Training and Validation Loss', fontsize=14)
axes[0].legend()
axes[0].grid(True, alpha=0.3)
# Plot accuracy
axes[1].plot(history['train_acc'], 'b-', label='Train Accuracy', linewidth=2)
axes[1].plot(history['val_acc'], 'r-', label='Val Accuracy', linewidth=2)
axes[1].set_xlabel('Epoch', fontsize=12)
axes[1].set_ylabel('Accuracy (%)', fontsize=12)
axes[1].set_title('Training and Validation Accuracy', fontsize=14)
axes[1].legend()
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
print(f"Final train accuracy: {history['train_acc'][-1]:.2f}%")
print(f"Final val accuracy: {history['val_acc'][-1]:.2f}%")
Final train accuracy: 93.58% Final val accuracy: 73.91%
11. Evaluate on Test Set¶
In [11]:
Copied!
def evaluate_model(model, test_loader, device='cpu'):
"""
Evaluate model on test set.
"""
model.eval()
all_preds = []
all_labels = []
with torch.no_grad():
for images, labels in test_loader:
images, labels = images.to(device), labels.to(device)
outputs = model(images)
_, predicted = torch.max(outputs.data, 1)
all_preds.extend(predicted.cpu().numpy())
all_labels.extend(labels.cpu().numpy())
return np.array(all_labels), np.array(all_preds)
# Evaluate on test set
print("🧪 Evaluating on test set...")
test_labels, test_preds = evaluate_model(model, test_loader, device)
# Calculate accuracy
test_acc = accuracy_score(test_labels, test_preds) * 100
print(f"\n✅ Test Accuracy: {test_acc:.2f}%")
# Classification report
print("\n📊 Classification Report:")
print(classification_report(test_labels, test_preds, target_names=class_names))
def evaluate_model(model, test_loader, device='cpu'):
"""
Evaluate model on test set.
"""
model.eval()
all_preds = []
all_labels = []
with torch.no_grad():
for images, labels in test_loader:
images, labels = images.to(device), labels.to(device)
outputs = model(images)
_, predicted = torch.max(outputs.data, 1)
all_preds.extend(predicted.cpu().numpy())
all_labels.extend(labels.cpu().numpy())
return np.array(all_labels), np.array(all_preds)
# Evaluate on test set
print("🧪 Evaluating on test set...")
test_labels, test_preds = evaluate_model(model, test_loader, device)
# Calculate accuracy
test_acc = accuracy_score(test_labels, test_preds) * 100
print(f"\n✅ Test Accuracy: {test_acc:.2f}%")
# Classification report
print("\n📊 Classification Report:")
print(classification_report(test_labels, test_preds, target_names=class_names))
🧪 Evaluating on test set...
✅ Test Accuracy: 91.67%
📊 Classification Report:
precision recall f1-score support
Black Soil 1.00 0.83 0.91 6
Cinder Soil 1.00 1.00 1.00 4
Laterite Soil 0.75 1.00 0.86 3
Peat Soil 0.80 0.80 0.80 5
Yellow Soil 1.00 1.00 1.00 6
accuracy 0.92 24
macro avg 0.91 0.93 0.91 24
weighted avg 0.93 0.92 0.92 24
12. Confusion Matrix¶
In [12]:
Copied!
# Compute confusion matrix
cm = confusion_matrix(test_labels, test_preds)
# Plot confusion matrix
plt.figure(figsize=(10, 8))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
xticklabels=class_names, yticklabels=class_names,
cbar_kws={'label': 'Count'})
plt.xlabel('Predicted Label', fontsize=12)
plt.ylabel('True Label', fontsize=12)
plt.title('Confusion Matrix - Soil Type Classification', fontsize=14)
plt.tight_layout()
plt.show()
# Compute confusion matrix
cm = confusion_matrix(test_labels, test_preds)
# Plot confusion matrix
plt.figure(figsize=(10, 8))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
xticklabels=class_names, yticklabels=class_names,
cbar_kws={'label': 'Count'})
plt.xlabel('Predicted Label', fontsize=12)
plt.ylabel('True Label', fontsize=12)
plt.title('Confusion Matrix - Soil Type Classification', fontsize=14)
plt.tight_layout()
plt.show()
13. Visualize Predictions on Test Images¶
In [13]:
Copied!
# Get a batch of test images
dataiter = iter(test_loader)
images, labels = next(dataiter)
# Make predictions
images_device = images.to(device)
model.eval()
with torch.no_grad():
outputs = model(images_device)
_, preds = torch.max(outputs, 1)
# Unnormalize images for display
def unnormalize(img):
img = img.numpy().transpose((1, 2, 0))
mean = np.array([0.485, 0.456, 0.406])
std = np.array([0.229, 0.224, 0.225])
img = std * img + mean
img = np.clip(img, 0, 1)
return img
# Plot predictions
fig, axes = plt.subplots(2, 4, figsize=(16, 8))
axes = axes.flatten()
for i in range(min(8, len(images))):
img = unnormalize(images[i].cpu())
true_label = class_names[labels[i]]
pred_label = class_names[preds[i]]
axes[i].imshow(img)
color = 'green' if true_label == pred_label else 'red'
axes[i].set_title(f'True: {true_label}\nPred: {pred_label}', color=color, fontsize=10)
axes[i].axis('off')
plt.tight_layout()
plt.show()
# Get a batch of test images
dataiter = iter(test_loader)
images, labels = next(dataiter)
# Make predictions
images_device = images.to(device)
model.eval()
with torch.no_grad():
outputs = model(images_device)
_, preds = torch.max(outputs, 1)
# Unnormalize images for display
def unnormalize(img):
img = img.numpy().transpose((1, 2, 0))
mean = np.array([0.485, 0.456, 0.406])
std = np.array([0.229, 0.224, 0.225])
img = std * img + mean
img = np.clip(img, 0, 1)
return img
# Plot predictions
fig, axes = plt.subplots(2, 4, figsize=(16, 8))
axes = axes.flatten()
for i in range(min(8, len(images))):
img = unnormalize(images[i].cpu())
true_label = class_names[labels[i]]
pred_label = class_names[preds[i]]
axes[i].imshow(img)
color = 'green' if true_label == pred_label else 'red'
axes[i].set_title(f'True: {true_label}\nPred: {pred_label}', color=color, fontsize=10)
axes[i].axis('off')
plt.tight_layout()
plt.show()
14. Summary and Key Takeaways¶
What We Learned¶
CNN Architecture for Image Classification:
- Convolutional layers extract spatial features
- Pooling layers reduce dimensionality
- Fully connected layers perform classification
Data Augmentation:
- Essential for small datasets
- Random flips, rotations, color jitter
- Prevents overfitting
Training Techniques:
- Batch normalization for stable training
- Dropout for regularization
- Learning rate scheduling
- Early stopping with best model saving
Evaluation:
- Confusion matrix shows per-class performance
- Classification report provides detailed metrics
- Visual inspection of predictions
Applications in Geotechnical Engineering¶
- Automated soil classification from field images
- Quality control in construction
- Rapid site characterization
- Remote sensing and drone-based surveys