mirror of
https://github.com/nprasad2077/MNIST.git
synced 2026-11-04 09:15:29 +00:00
initial training complete
This commit is contained in:
1 parent
ec913e3eb7
commit
73423bd316
17 files changed
+23157
-1
No files matched your search
+13
@@ -216,3 +216,16 @@ __marimo__/
|
||||
|
||||
# Streamlit
|
||||
.streamlit/secrets.toml
|
||||
|
||||
|
||||
docs\prompts\
|
||||
docs/prompts/
|
||||
docs\prompts
|
||||
docs/prompts
|
||||
|
||||
data\
|
||||
data
|
||||
|
||||
setup.ps1
|
||||
*.ps1
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
default_profile = "duckdb"
|
||||
|
||||
[profiles.duckdb]
|
||||
adapter = "duckdb"
|
||||
theme = "textual-light"
|
||||
limit = 100000
|
||||
keymap_name = ["vscode"]
|
||||
locale = "C"
|
||||
@@ -1 +1,100 @@
|
||||
# MNIST
|
||||
# MNIST Handwritten Digit Classification
|
||||
|
||||
A learning project for training a neural network on the MNIST dataset using PyTorch and DuckDB, with Jupyter notebooks for step-by-step visualization.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```powershell
|
||||
# 1. Run the setup script (creates venv, installs deps, extracts data)
|
||||
.\setup.ps1
|
||||
|
||||
# 2. Open VSCode in this folder
|
||||
code .
|
||||
|
||||
# 3. Open notebooks/01_data_loading.ipynb
|
||||
# Select kernel: "MNIST (Python)"
|
||||
# Run all cells
|
||||
```
|
||||
|
||||
## Manual Setup
|
||||
|
||||
If you prefer to set up manually:
|
||||
|
||||
```powershell
|
||||
# Create and activate virtual environment
|
||||
python -m venv .venv
|
||||
.\.venv\Scripts\Activate.ps1
|
||||
|
||||
# Install dependencies
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Register Jupyter kernel for VSCode
|
||||
python -m ipykernel install --user --name mnist --display-name "MNIST (Python)"
|
||||
|
||||
# Extract data (if not already done)
|
||||
Expand-Archive -Path data\MNIST_ORG.zip -DestinationPath data -Force
|
||||
```
|
||||
|
||||
## VSCode Notebook Setup
|
||||
|
||||
1. Install the **Jupyter** extension in VSCode (`ms-toolsai.jupyter`)
|
||||
2. Open any `.ipynb` file
|
||||
3. Click "Select Kernel" → "Jupyter Kernel" → **MNIST (Python)** (view kernels installed: `jupyter kernelspec list`. May need to refresh list.)
|
||||
4. Run cells with `Shift+Enter`
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
MNIST/
|
||||
├── data/
|
||||
│ ├── MNIST_ORG.zip # Original compressed data
|
||||
│ ├── train-images.idx3-ubyte # 60,000 training images (28x28)
|
||||
│ ├── train-labels.idx1-ubyte # Training labels (0-9)
|
||||
│ ├── t10k-images.idx3-ubyte # 10,000 test images
|
||||
│ ├── t10k-labels.idx1-ubyte # Test labels
|
||||
│ └── mnist.duckdb # Generated after running notebook 01
|
||||
├── notebooks/
|
||||
│ ├── 01_data_loading.ipynb # Parse IDX files → DuckDB, explore with SQL
|
||||
│ └── 02_model_training.ipynb # Train neural net, visualize results
|
||||
├── requirements.txt
|
||||
├── setup.ps1
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Notebooks
|
||||
|
||||
### 01 - Data Loading & DuckDB Storage
|
||||
- Parses raw MNIST IDX binary format
|
||||
- Stores all 70,000 images in DuckDB (queryable with SQL)
|
||||
- Explores label distribution and pixel intensity
|
||||
- Visualizes sample digits
|
||||
|
||||
### 02 - Model Training & Visualization
|
||||
- Loads data from DuckDB into PyTorch
|
||||
- Trains a fully-connected neural network (784 → 256 → 128 → 10)
|
||||
- Tracks loss and accuracy per epoch
|
||||
- Visualizes training curves, confusion matrix, and misclassified examples
|
||||
- Saves model weights and training history
|
||||
|
||||
## Key Hyperparameters to Experiment With
|
||||
|
||||
| Parameter | Default | Effect |
|
||||
|-----------|---------|--------|
|
||||
| Learning Rate | 0.001 | Step size for optimization |
|
||||
| Hidden Layers | 256, 128 | Model capacity |
|
||||
| Dropout | 0.2 | Regularization strength |
|
||||
| Batch Size | 128 | Gradient noise vs speed |
|
||||
| Epochs | 15 | Training duration |
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **DuckDB** - In-process SQL database for data storage and exploration
|
||||
- **PyTorch** - Neural network training framework
|
||||
- **Matplotlib/Seaborn** - Visualization
|
||||
- **Jupyter** - Interactive notebook environment in VSCode
|
||||
|
||||
## References
|
||||
|
||||
- [MNIST Database](http://yann.lecun.com/exdb/mnist/)
|
||||
- [PyTorch Documentation](https://pytorch.org/docs/stable/)
|
||||
- [DuckDB Documentation](https://duckdb.org/docs/)
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,171 @@
|
||||
# 01 - MNIST Data Loading & DuckDB Storage
|
||||
|
||||
This notebook:
|
||||
|
||||
1. Reads raw MNIST IDX binary files
|
||||
2. Stores them in DuckDB for fast SQL-based exploration
|
||||
3. Visualizes sample digits
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import duckdb
|
||||
import struct
|
||||
import matplotlib.pyplot as plt
|
||||
from pathlib import Path
|
||||
```
|
||||
|
||||
## Load MNIST IDX Files
|
||||
|
||||
The IDX format stores images as raw bytes with a header containing dimensions.
|
||||
|
||||
```python
|
||||
def read_idx_images(path):
|
||||
with open(path, 'rb') as f:
|
||||
magic, n, rows, cols = struct.unpack('>IIII', f.read(16))
|
||||
return np.frombuffer(f.read(), dtype=np.uint8).reshape(n, rows * cols)
|
||||
|
||||
def read_idx_labels(path):
|
||||
with open(path, 'rb') as f:
|
||||
magic, n = struct.unpack('>II', f.read(8))
|
||||
return np.frombuffer(f.read(), dtype=np.uint8)
|
||||
|
||||
data_dir = Path('../data')
|
||||
train_images = read_idx_images(data_dir / 'train-images.idx3-ubyte')
|
||||
train_labels = read_idx_labels(data_dir / 'train-labels.idx1-ubyte')
|
||||
test_images = read_idx_images(data_dir / 't10k-images.idx3-ubyte')
|
||||
test_labels = read_idx_labels(data_dir / 't10k-labels.idx1-ubyte')
|
||||
|
||||
print(f'Train: {train_images.shape[0]} images, {train_images.shape[1]} pixels each')
|
||||
print(f'Test: {test_images.shape[0]} images')
|
||||
```
|
||||
|
||||
Train: 60000 images, 784 pixels each
|
||||
Test: 10000 images
|
||||
|
||||
|
||||
## Store in DuckDB
|
||||
|
||||
We store pixel data as a flat array (784 columns for 28x28) plus the label. This lets us query and filter with SQL.
|
||||
|
||||
```python
|
||||
db_path = str(data_dir / 'mnist.duckdb')
|
||||
con = duckdb.connect(db_path)
|
||||
|
||||
# Create train table
|
||||
train_df = pd.DataFrame(train_images, columns=[f'px{i}' for i in range(784)])
|
||||
train_df['label'] = train_labels
|
||||
train_df['split'] = 'train'
|
||||
|
||||
# Create test table
|
||||
test_df = pd.DataFrame(test_images, columns=[f'px{i}' for i in range(784)])
|
||||
test_df['label'] = test_labels
|
||||
test_df['split'] = 'test'
|
||||
|
||||
# Combine and write
|
||||
all_df = pd.concat([train_df, test_df], ignore_index=True)
|
||||
con.execute('DROP TABLE IF EXISTS mnist')
|
||||
con.execute('CREATE TABLE mnist AS SELECT * FROM all_df')
|
||||
print(f'Stored {len(all_df)} records in DuckDB')
|
||||
```
|
||||
|
||||
Stored 70000 records in DuckDB
|
||||
|
||||
|
||||
## Explore with SQL
|
||||
|
||||
```python
|
||||
# Label distribution
|
||||
con.execute("""
|
||||
SELECT label, split, COUNT(*) as count
|
||||
FROM mnist
|
||||
GROUP BY label, split
|
||||
ORDER BY label, split
|
||||
""").df()
|
||||
```
|
||||
|
||||
| | label | split | count |
|
||||
| --- | ----- | ----- | ----- |
|
||||
| 0 | 0 | test | 980 |
|
||||
| 1 | 0 | train | 5923 |
|
||||
| 2 | 1 | test | 1135 |
|
||||
| 3 | 1 | train | 6742 |
|
||||
| 4 | 2 | test | 1032 |
|
||||
| 5 | 2 | train | 5958 |
|
||||
| 6 | 3 | test | 1010 |
|
||||
| 7 | 3 | train | 6131 |
|
||||
| 8 | 4 | test | 982 |
|
||||
| 9 | 4 | train | 5842 |
|
||||
| 10 | 5 | test | 892 |
|
||||
| 11 | 5 | train | 5421 |
|
||||
| 12 | 6 | test | 958 |
|
||||
| 13 | 6 | train | 5918 |
|
||||
| 14 | 7 | test | 1028 |
|
||||
| 15 | 7 | train | 6265 |
|
||||
| 16 | 8 | test | 974 |
|
||||
| 17 | 8 | train | 5851 |
|
||||
| 18 | 9 | test | 1009 |
|
||||
| 19 | 9 | train | 5949 |
|
||||
|
||||
```python
|
||||
# Average pixel intensity per digit
|
||||
con.execute("""
|
||||
SELECT label,
|
||||
ROUND(AVG(px196::INT + px197::INT + px224::INT + px225::INT), 1) as center_intensity
|
||||
FROM mnist WHERE split='train'
|
||||
GROUP BY label ORDER BY label
|
||||
""").df()
|
||||
```
|
||||
|
||||
| | label | center_intensity |
|
||||
| --- | ----- | ---------------- |
|
||||
| 0 | 0 | 0.0 |
|
||||
| 1 | 1 | 0.0 |
|
||||
| 2 | 2 | 0.0 |
|
||||
| 3 | 3 | 0.1 |
|
||||
| 4 | 4 | 0.0 |
|
||||
| 5 | 5 | 0.0 |
|
||||
| 6 | 6 | 0.0 |
|
||||
| 7 | 7 | 0.6 |
|
||||
| 8 | 8 | 0.0 |
|
||||
| 9 | 9 | 0.0 |
|
||||
|
||||
## Visualize Sample Digits
|
||||
|
||||
```python
|
||||
fig, axes = plt.subplots(2, 5, figsize=(10, 4))
|
||||
for digit in range(10):
|
||||
row = con.execute(f"SELECT * FROM mnist WHERE label={digit} AND split='train' LIMIT 1").fetchone()
|
||||
pixels = np.array(row[:784]).reshape(28, 28)
|
||||
ax = axes[digit // 5, digit % 5]
|
||||
ax.imshow(pixels, cmap='gray')
|
||||
ax.set_title(f'Label: {digit}')
|
||||
ax.axis('off')
|
||||
plt.tight_layout()
|
||||
plt.show()
|
||||
```
|
||||
|
||||

|
||||
|
||||
```python
|
||||
# Pixel intensity heatmap averaged across all training images
|
||||
avg_pixels = con.execute("""
|
||||
SELECT {} FROM mnist WHERE split='train'
|
||||
""".format(', '.join(f'AVG(px{i}) as px{i}' for i in range(784)))).fetchone()
|
||||
|
||||
plt.figure(figsize=(5, 5))
|
||||
plt.imshow(np.array(avg_pixels).reshape(28, 28), cmap='hot')
|
||||
plt.title('Average pixel intensity (all digits)')
|
||||
plt.colorbar()
|
||||
plt.show()
|
||||
```
|
||||
|
||||

|
||||
|
||||
```python
|
||||
con.close()
|
||||
print('DuckDB connection closed. Database saved at:', db_path)
|
||||
```
|
||||
|
||||
DuckDB connection closed. Database saved at: ..\data\mnist.duckdb
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,340 @@
|
||||
# 02 - MNIST Model Training & Visualization
|
||||
|
||||
This notebook trains a neural network on MNIST data loaded from DuckDB, then visualizes:
|
||||
|
||||
- Training loss & accuracy curves
|
||||
- Confusion matrix
|
||||
- Per-digit accuracy
|
||||
- Misclassified examples
|
||||
|
||||
**Key ML Concepts Covered:**
|
||||
|
||||
- Neural network architecture (layers, activations)
|
||||
- Forward/backward propagation
|
||||
- Loss functions & optimizers
|
||||
- Batch training with DataLoaders
|
||||
- Overfitting detection (train vs validation)
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import duckdb
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from torch.utils.data import DataLoader, TensorDataset
|
||||
import matplotlib.pyplot as plt
|
||||
import seaborn as sns
|
||||
from sklearn.metrics import confusion_matrix, classification_report
|
||||
from pathlib import Path
|
||||
from tqdm.notebook import tqdm
|
||||
```
|
||||
|
||||
## 1. Load Data from DuckDB
|
||||
|
||||
```python
|
||||
con = duckdb.connect(str(Path('../data/mnist.duckdb')), read_only=True)
|
||||
|
||||
train_data = con.execute("SELECT * FROM mnist WHERE split='train'").fetchnumpy()
|
||||
test_data = con.execute("SELECT * FROM mnist WHERE split='test'").fetchnumpy()
|
||||
con.close()
|
||||
|
||||
# Extract pixel columns and labels
|
||||
px_cols = [f'px{i}' for i in range(784)]
|
||||
X_train = np.column_stack([train_data[c] for c in px_cols]).astype(np.float32) / 255.0
|
||||
y_train = train_data['label'].astype(np.int64)
|
||||
X_test = np.column_stack([test_data[c] for c in px_cols]).astype(np.float32) / 255.0
|
||||
y_test = test_data['label'].astype(np.int64)
|
||||
|
||||
print(f'Train: {X_train.shape}, Test: {X_test.shape}')
|
||||
```
|
||||
|
||||
Train: (60000, 784), Test: (10000, 784)
|
||||
|
||||
|
||||
## 2. Create PyTorch DataLoaders
|
||||
|
||||
DataLoaders handle batching and shuffling. Batch size is a key hyperparameter:
|
||||
|
||||
- **Smaller batches** → noisier gradients, can help escape local minima
|
||||
- **Larger batches** → smoother gradients, faster training per epoch
|
||||
|
||||
```python
|
||||
BATCH_SIZE = 128
|
||||
|
||||
train_ds = TensorDataset(torch.tensor(X_train), torch.tensor(y_train))
|
||||
test_ds = TensorDataset(torch.tensor(X_test), torch.tensor(y_test))
|
||||
|
||||
train_loader = DataLoader(train_ds, batch_size=BATCH_SIZE, shuffle=True)
|
||||
test_loader = DataLoader(test_ds, batch_size=BATCH_SIZE)
|
||||
```
|
||||
|
||||
## 3. Define the Neural Network
|
||||
|
||||
Architecture: 784 → 256 → 128 → 10
|
||||
|
||||
**Key parameters to experiment with:**
|
||||
|
||||
- Hidden layer sizes (try 64, 128, 256, 512)
|
||||
- Number of layers (try 1-4 hidden layers)
|
||||
- Activation function (ReLU, Sigmoid, Tanh)
|
||||
- Dropout rate (regularization to prevent overfitting)
|
||||
|
||||
```python
|
||||
class MNISTNet(nn.Module):
|
||||
def __init__(self, hidden1=256, hidden2=128, dropout=0.2):
|
||||
super().__init__()
|
||||
self.net = nn.Sequential(
|
||||
nn.Linear(784, hidden1),
|
||||
nn.ReLU(),
|
||||
nn.Dropout(dropout),
|
||||
nn.Linear(hidden1, hidden2),
|
||||
nn.ReLU(),
|
||||
nn.Dropout(dropout),
|
||||
nn.Linear(hidden2, 10)
|
||||
)
|
||||
|
||||
def forward(self, x):
|
||||
return self.net(x)
|
||||
|
||||
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
||||
print(f'Using device: {device}')
|
||||
```
|
||||
|
||||
Using device: cpu
|
||||
|
||||
|
||||
## 4. Training Loop
|
||||
|
||||
**Hyperparameters:**
|
||||
|
||||
- `lr` (learning rate): How big each optimization step is. Too high → diverges. Too low → slow.
|
||||
- `epochs`: Number of full passes through the training data.
|
||||
- `optimizer`: Adam adapts learning rate per-parameter. SGD is simpler but needs tuning.
|
||||
|
||||
```python
|
||||
# === HYPERPARAMETERS (experiment with these!) ===
|
||||
EPOCHS = 15
|
||||
LEARNING_RATE = 1e-3
|
||||
HIDDEN1 = 256
|
||||
HIDDEN2 = 128
|
||||
DROPOUT = 0.2
|
||||
|
||||
model = MNISTNet(HIDDEN1, HIDDEN2, DROPOUT).to(device)
|
||||
criterion = nn.CrossEntropyLoss()
|
||||
optimizer = torch.optim.Adam(model.parameters(), lr=LEARNING_RATE)
|
||||
|
||||
print(f'Model parameters: {sum(p.numel() for p in model.parameters()):,}')
|
||||
print(model)
|
||||
```
|
||||
|
||||
Model parameters: 235,146
|
||||
MNISTNet(
|
||||
(net): Sequential(
|
||||
(0): Linear(in_features=784, out_features=256, bias=True)
|
||||
(1): ReLU()
|
||||
(2): Dropout(p=0.2, inplace=False)
|
||||
(3): Linear(in_features=256, out_features=128, bias=True)
|
||||
(4): ReLU()
|
||||
(5): Dropout(p=0.2, inplace=False)
|
||||
(6): Linear(in_features=128, out_features=10, bias=True)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
```python
|
||||
# Training with metrics tracking
|
||||
history = {'train_loss': [], 'train_acc': [], 'test_loss': [], 'test_acc': []}
|
||||
|
||||
for epoch in tqdm(range(EPOCHS), desc='Training'):
|
||||
# Train
|
||||
model.train()
|
||||
running_loss, correct, total = 0.0, 0, 0
|
||||
for X_batch, y_batch in train_loader:
|
||||
X_batch, y_batch = X_batch.to(device), y_batch.to(device)
|
||||
optimizer.zero_grad()
|
||||
outputs = model(X_batch)
|
||||
loss = criterion(outputs, y_batch)
|
||||
loss.backward()
|
||||
optimizer.step()
|
||||
running_loss += loss.item() * len(y_batch)
|
||||
correct += (outputs.argmax(1) == y_batch).sum().item()
|
||||
total += len(y_batch)
|
||||
history['train_loss'].append(running_loss / total)
|
||||
history['train_acc'].append(correct / total)
|
||||
|
||||
# Evaluate
|
||||
model.eval()
|
||||
running_loss, correct, total = 0.0, 0, 0
|
||||
with torch.no_grad():
|
||||
for X_batch, y_batch in test_loader:
|
||||
X_batch, y_batch = X_batch.to(device), y_batch.to(device)
|
||||
outputs = model(X_batch)
|
||||
loss = criterion(outputs, y_batch)
|
||||
running_loss += loss.item() * len(y_batch)
|
||||
correct += (outputs.argmax(1) == y_batch).sum().item()
|
||||
total += len(y_batch)
|
||||
history['test_loss'].append(running_loss / total)
|
||||
history['test_acc'].append(correct / total)
|
||||
|
||||
print(f"Epoch {epoch+1:2d} | Train Loss: {history['train_loss'][-1]:.4f} Acc: {history['train_acc'][-1]:.4f} | "
|
||||
f"Test Loss: {history['test_loss'][-1]:.4f} Acc: {history['test_acc'][-1]:.4f}")
|
||||
```
|
||||
|
||||
Training: 0%| | 0/15 [00:00<?, ?it/s]
|
||||
|
||||
|
||||
Epoch 1 | Train Loss: 0.3803 Acc: 0.8898 | Test Loss: 0.1511 Acc: 0.9547
|
||||
Epoch 2 | Train Loss: 0.1498 Acc: 0.9547 | Test Loss: 0.1011 Acc: 0.9693
|
||||
Epoch 3 | Train Loss: 0.1079 Acc: 0.9671 | Test Loss: 0.0837 Acc: 0.9731
|
||||
Epoch 4 | Train Loss: 0.0836 Acc: 0.9739 | Test Loss: 0.0759 Acc: 0.9762
|
||||
Epoch 5 | Train Loss: 0.0703 Acc: 0.9781 | Test Loss: 0.0729 Acc: 0.9775
|
||||
Epoch 6 | Train Loss: 0.0585 Acc: 0.9814 | Test Loss: 0.0722 Acc: 0.9782
|
||||
Epoch 7 | Train Loss: 0.0532 Acc: 0.9825 | Test Loss: 0.0709 Acc: 0.9804
|
||||
Epoch 8 | Train Loss: 0.0444 Acc: 0.9861 | Test Loss: 0.0724 Acc: 0.9783
|
||||
Epoch 9 | Train Loss: 0.0422 Acc: 0.9861 | Test Loss: 0.0615 Acc: 0.9816
|
||||
Epoch 10 | Train Loss: 0.0350 Acc: 0.9888 | Test Loss: 0.0631 Acc: 0.9828
|
||||
Epoch 11 | Train Loss: 0.0358 Acc: 0.9887 | Test Loss: 0.0622 Acc: 0.9821
|
||||
Epoch 12 | Train Loss: 0.0307 Acc: 0.9894 | Test Loss: 0.0670 Acc: 0.9817
|
||||
Epoch 13 | Train Loss: 0.0294 Acc: 0.9904 | Test Loss: 0.0677 Acc: 0.9827
|
||||
Epoch 14 | Train Loss: 0.0264 Acc: 0.9906 | Test Loss: 0.0659 Acc: 0.9818
|
||||
Epoch 15 | Train Loss: 0.0251 Acc: 0.9916 | Test Loss: 0.0653 Acc: 0.9826
|
||||
|
||||
|
||||
## 5. Training Curves
|
||||
|
||||
**What to look for:**
|
||||
|
||||
- If test loss increases while train loss decreases → **overfitting** (try more dropout, fewer parameters)
|
||||
- If both losses plateau high → **underfitting** (try more parameters, lower dropout)
|
||||
- Gap between train/test accuracy shows generalization
|
||||
|
||||
```python
|
||||
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))
|
||||
|
||||
ax1.plot(history['train_loss'], label='Train')
|
||||
ax1.plot(history['test_loss'], label='Test')
|
||||
ax1.set_xlabel('Epoch')
|
||||
ax1.set_ylabel('Loss')
|
||||
ax1.set_title('Loss Curve')
|
||||
ax1.legend()
|
||||
ax1.grid(True, alpha=0.3)
|
||||
|
||||
ax2.plot(history['train_acc'], label='Train')
|
||||
ax2.plot(history['test_acc'], label='Test')
|
||||
ax2.set_xlabel('Epoch')
|
||||
ax2.set_ylabel('Accuracy')
|
||||
ax2.set_title('Accuracy Curve')
|
||||
ax2.legend()
|
||||
ax2.grid(True, alpha=0.3)
|
||||
|
||||
plt.tight_layout()
|
||||
plt.show()
|
||||
```
|
||||
|
||||

|
||||
|
||||
## 6. Confusion Matrix
|
||||
|
||||
Shows which digits get confused with each other (e.g., 4↔9, 3↔5 are common).
|
||||
|
||||
```python
|
||||
model.eval()
|
||||
all_preds, all_labels = [], []
|
||||
with torch.no_grad():
|
||||
for X_batch, y_batch in test_loader:
|
||||
preds = model(X_batch.to(device)).argmax(1).cpu()
|
||||
all_preds.append(preds)
|
||||
all_labels.append(y_batch)
|
||||
|
||||
all_preds = torch.cat(all_preds).numpy()
|
||||
all_labels = torch.cat(all_labels).numpy()
|
||||
|
||||
cm = confusion_matrix(all_labels, all_preds)
|
||||
plt.figure(figsize=(8, 6))
|
||||
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
|
||||
xticklabels=range(10), yticklabels=range(10))
|
||||
plt.xlabel('Predicted')
|
||||
plt.ylabel('Actual')
|
||||
plt.title('Confusion Matrix')
|
||||
plt.show()
|
||||
```
|
||||
|
||||

|
||||
|
||||
```python
|
||||
print(classification_report(all_labels, all_preds, digits=4))
|
||||
```
|
||||
|
||||
precision recall f1-score support
|
||||
|
||||
0 0.9798 0.9908 0.9853 980
|
||||
1 0.9877 0.9930 0.9903 1135
|
||||
2 0.9872 0.9700 0.9785 1032
|
||||
3 0.9861 0.9832 0.9846 1010
|
||||
4 0.9825 0.9725 0.9775 982
|
||||
5 0.9898 0.9798 0.9848 892
|
||||
6 0.9864 0.9875 0.9870 958
|
||||
7 0.9806 0.9815 0.9810 1028
|
||||
8 0.9785 0.9805 0.9795 974
|
||||
9 0.9679 0.9861 0.9769 1009
|
||||
|
||||
accuracy 0.9826 10000
|
||||
macro avg 0.9827 0.9825 0.9825 10000
|
||||
weighted avg 0.9826 0.9826 0.9826 10000
|
||||
|
||||
|
||||
|
||||
## 7. Misclassified Examples
|
||||
|
||||
Seeing what the model gets wrong helps understand its weaknesses.
|
||||
|
||||
```python
|
||||
wrong_idx = np.where(all_preds != all_labels)[0][:10]
|
||||
|
||||
fig, axes = plt.subplots(2, 5, figsize=(12, 5))
|
||||
for i, idx in enumerate(wrong_idx):
|
||||
ax = axes[i // 5, i % 5]
|
||||
ax.imshow(X_test[idx].reshape(28, 28), cmap='gray')
|
||||
ax.set_title(f'True: {all_labels[idx]}\nPred: {all_preds[idx]}', color='red')
|
||||
ax.axis('off')
|
||||
plt.suptitle('Misclassified Examples', fontsize=14)
|
||||
plt.tight_layout()
|
||||
plt.show()
|
||||
```
|
||||
|
||||

|
||||
|
||||
## 8. Save Model & Training Results to DuckDB
|
||||
|
||||
```python
|
||||
import pandas as pd
|
||||
|
||||
# Save training history
|
||||
history_df = pd.DataFrame(history)
|
||||
history_df['epoch'] = range(1, EPOCHS + 1)
|
||||
|
||||
con = duckdb.connect(str(Path('../data/mnist.duckdb')))
|
||||
con.execute('DROP TABLE IF EXISTS training_history')
|
||||
con.execute('CREATE TABLE training_history AS SELECT * FROM history_df')
|
||||
con.close()
|
||||
|
||||
# Save model weights
|
||||
torch.save(model.state_dict(), '../data/mnist_model.pth')
|
||||
print(f'Model saved. Final test accuracy: {history["test_acc"][-1]:.4f}')
|
||||
```
|
||||
|
||||
Model saved. Final test accuracy: 0.9826
|
||||
|
||||
|
||||
## 9. Experiment Ideas
|
||||
|
||||
Try changing these parameters and re-running to see the effect:
|
||||
|
||||
| Parameter | Current | Try |
|
||||
| --------------- | ------- | -------------------------- |
|
||||
| `LEARNING_RATE` | 0.001 | 0.01, 0.0001 |
|
||||
| `HIDDEN1` | 256 | 64, 512 |
|
||||
| `HIDDEN2` | 128 | 32, 256 |
|
||||
| `DROPOUT` | 0.2 | 0.0, 0.5 |
|
||||
| `EPOCHS` | 15 | 5, 30 |
|
||||
| `BATCH_SIZE` | 128 | 32, 512 |
|
||||
| Optimizer | Adam | SGD(lr=0.01, momentum=0.9) |
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,537 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# 01 - MNIST Data Loading & DuckDB Storage\n",
|
||||
"\n",
|
||||
"This notebook:\n",
|
||||
"1. Reads raw MNIST IDX binary files\n",
|
||||
"2. Stores them in DuckDB for fast SQL-based exploration\n",
|
||||
"3. Visualizes sample digits"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import numpy as np\n",
|
||||
"import pandas as pd\n",
|
||||
"import duckdb\n",
|
||||
"import struct\n",
|
||||
"import matplotlib.pyplot as plt\n",
|
||||
"from pathlib import Path"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Load MNIST IDX Files\n",
|
||||
"\n",
|
||||
"The IDX format stores images as raw bytes with a header containing dimensions."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Train: 60000 images, 784 pixels each\n",
|
||||
"Test: 10000 images\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"def read_idx_images(path):\n",
|
||||
" with open(path, 'rb') as f:\n",
|
||||
" magic, n, rows, cols = struct.unpack('>IIII', f.read(16))\n",
|
||||
" return np.frombuffer(f.read(), dtype=np.uint8).reshape(n, rows * cols)\n",
|
||||
"\n",
|
||||
"def read_idx_labels(path):\n",
|
||||
" with open(path, 'rb') as f:\n",
|
||||
" magic, n = struct.unpack('>II', f.read(8))\n",
|
||||
" return np.frombuffer(f.read(), dtype=np.uint8)\n",
|
||||
"\n",
|
||||
"data_dir = Path('../data')\n",
|
||||
"train_images = read_idx_images(data_dir / 'train-images.idx3-ubyte')\n",
|
||||
"train_labels = read_idx_labels(data_dir / 'train-labels.idx1-ubyte')\n",
|
||||
"test_images = read_idx_images(data_dir / 't10k-images.idx3-ubyte')\n",
|
||||
"test_labels = read_idx_labels(data_dir / 't10k-labels.idx1-ubyte')\n",
|
||||
"\n",
|
||||
"print(f'Train: {train_images.shape[0]} images, {train_images.shape[1]} pixels each')\n",
|
||||
"print(f'Test: {test_images.shape[0]} images')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Store in DuckDB\n",
|
||||
"\n",
|
||||
"We store pixel data as a flat array (784 columns for 28x28) plus the label. This lets us query and filter with SQL."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Stored 70000 records in DuckDB\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"db_path = str(data_dir / 'mnist.duckdb')\n",
|
||||
"con = duckdb.connect(db_path)\n",
|
||||
"\n",
|
||||
"# Create train table\n",
|
||||
"train_df = pd.DataFrame(train_images, columns=[f'px{i}' for i in range(784)])\n",
|
||||
"train_df['label'] = train_labels\n",
|
||||
"train_df['split'] = 'train'\n",
|
||||
"\n",
|
||||
"# Create test table\n",
|
||||
"test_df = pd.DataFrame(test_images, columns=[f'px{i}' for i in range(784)])\n",
|
||||
"test_df['label'] = test_labels\n",
|
||||
"test_df['split'] = 'test'\n",
|
||||
"\n",
|
||||
"# Combine and write\n",
|
||||
"all_df = pd.concat([train_df, test_df], ignore_index=True)\n",
|
||||
"con.execute('DROP TABLE IF EXISTS mnist')\n",
|
||||
"con.execute('CREATE TABLE mnist AS SELECT * FROM all_df')\n",
|
||||
"print(f'Stored {len(all_df)} records in DuckDB')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Explore with SQL"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/html": [
|
||||
"<div>\n",
|
||||
"<style scoped>\n",
|
||||
" .dataframe tbody tr th:only-of-type {\n",
|
||||
" vertical-align: middle;\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" .dataframe tbody tr th {\n",
|
||||
" vertical-align: top;\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" .dataframe thead th {\n",
|
||||
" text-align: right;\n",
|
||||
" }\n",
|
||||
"</style>\n",
|
||||
"<table border=\"1\" class=\"dataframe\">\n",
|
||||
" <thead>\n",
|
||||
" <tr style=\"text-align: right;\">\n",
|
||||
" <th></th>\n",
|
||||
" <th>label</th>\n",
|
||||
" <th>split</th>\n",
|
||||
" <th>count</th>\n",
|
||||
" </tr>\n",
|
||||
" </thead>\n",
|
||||
" <tbody>\n",
|
||||
" <tr>\n",
|
||||
" <th>0</th>\n",
|
||||
" <td>0</td>\n",
|
||||
" <td>test</td>\n",
|
||||
" <td>980</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>1</th>\n",
|
||||
" <td>0</td>\n",
|
||||
" <td>train</td>\n",
|
||||
" <td>5923</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>2</th>\n",
|
||||
" <td>1</td>\n",
|
||||
" <td>test</td>\n",
|
||||
" <td>1135</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>3</th>\n",
|
||||
" <td>1</td>\n",
|
||||
" <td>train</td>\n",
|
||||
" <td>6742</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>4</th>\n",
|
||||
" <td>2</td>\n",
|
||||
" <td>test</td>\n",
|
||||
" <td>1032</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>5</th>\n",
|
||||
" <td>2</td>\n",
|
||||
" <td>train</td>\n",
|
||||
" <td>5958</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>6</th>\n",
|
||||
" <td>3</td>\n",
|
||||
" <td>test</td>\n",
|
||||
" <td>1010</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>7</th>\n",
|
||||
" <td>3</td>\n",
|
||||
" <td>train</td>\n",
|
||||
" <td>6131</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>8</th>\n",
|
||||
" <td>4</td>\n",
|
||||
" <td>test</td>\n",
|
||||
" <td>982</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>9</th>\n",
|
||||
" <td>4</td>\n",
|
||||
" <td>train</td>\n",
|
||||
" <td>5842</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>10</th>\n",
|
||||
" <td>5</td>\n",
|
||||
" <td>test</td>\n",
|
||||
" <td>892</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>11</th>\n",
|
||||
" <td>5</td>\n",
|
||||
" <td>train</td>\n",
|
||||
" <td>5421</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>12</th>\n",
|
||||
" <td>6</td>\n",
|
||||
" <td>test</td>\n",
|
||||
" <td>958</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>13</th>\n",
|
||||
" <td>6</td>\n",
|
||||
" <td>train</td>\n",
|
||||
" <td>5918</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>14</th>\n",
|
||||
" <td>7</td>\n",
|
||||
" <td>test</td>\n",
|
||||
" <td>1028</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>15</th>\n",
|
||||
" <td>7</td>\n",
|
||||
" <td>train</td>\n",
|
||||
" <td>6265</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>16</th>\n",
|
||||
" <td>8</td>\n",
|
||||
" <td>test</td>\n",
|
||||
" <td>974</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>17</th>\n",
|
||||
" <td>8</td>\n",
|
||||
" <td>train</td>\n",
|
||||
" <td>5851</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>18</th>\n",
|
||||
" <td>9</td>\n",
|
||||
" <td>test</td>\n",
|
||||
" <td>1009</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>19</th>\n",
|
||||
" <td>9</td>\n",
|
||||
" <td>train</td>\n",
|
||||
" <td>5949</td>\n",
|
||||
" </tr>\n",
|
||||
" </tbody>\n",
|
||||
"</table>\n",
|
||||
"</div>"
|
||||
],
|
||||
"text/plain": [
|
||||
" label split count\n",
|
||||
"0 0 test 980\n",
|
||||
"1 0 train 5923\n",
|
||||
"2 1 test 1135\n",
|
||||
"3 1 train 6742\n",
|
||||
"4 2 test 1032\n",
|
||||
"5 2 train 5958\n",
|
||||
"6 3 test 1010\n",
|
||||
"7 3 train 6131\n",
|
||||
"8 4 test 982\n",
|
||||
"9 4 train 5842\n",
|
||||
"10 5 test 892\n",
|
||||
"11 5 train 5421\n",
|
||||
"12 6 test 958\n",
|
||||
"13 6 train 5918\n",
|
||||
"14 7 test 1028\n",
|
||||
"15 7 train 6265\n",
|
||||
"16 8 test 974\n",
|
||||
"17 8 train 5851\n",
|
||||
"18 9 test 1009\n",
|
||||
"19 9 train 5949"
|
||||
]
|
||||
},
|
||||
"execution_count": 4,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Label distribution\n",
|
||||
"con.execute(\"\"\"\n",
|
||||
" SELECT label, split, COUNT(*) as count\n",
|
||||
" FROM mnist\n",
|
||||
" GROUP BY label, split\n",
|
||||
" ORDER BY label, split\n",
|
||||
"\"\"\").df()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/html": [
|
||||
"<div>\n",
|
||||
"<style scoped>\n",
|
||||
" .dataframe tbody tr th:only-of-type {\n",
|
||||
" vertical-align: middle;\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" .dataframe tbody tr th {\n",
|
||||
" vertical-align: top;\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" .dataframe thead th {\n",
|
||||
" text-align: right;\n",
|
||||
" }\n",
|
||||
"</style>\n",
|
||||
"<table border=\"1\" class=\"dataframe\">\n",
|
||||
" <thead>\n",
|
||||
" <tr style=\"text-align: right;\">\n",
|
||||
" <th></th>\n",
|
||||
" <th>label</th>\n",
|
||||
" <th>center_intensity</th>\n",
|
||||
" </tr>\n",
|
||||
" </thead>\n",
|
||||
" <tbody>\n",
|
||||
" <tr>\n",
|
||||
" <th>0</th>\n",
|
||||
" <td>0</td>\n",
|
||||
" <td>0.0</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>1</th>\n",
|
||||
" <td>1</td>\n",
|
||||
" <td>0.0</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>2</th>\n",
|
||||
" <td>2</td>\n",
|
||||
" <td>0.0</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>3</th>\n",
|
||||
" <td>3</td>\n",
|
||||
" <td>0.1</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>4</th>\n",
|
||||
" <td>4</td>\n",
|
||||
" <td>0.0</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>5</th>\n",
|
||||
" <td>5</td>\n",
|
||||
" <td>0.0</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>6</th>\n",
|
||||
" <td>6</td>\n",
|
||||
" <td>0.0</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>7</th>\n",
|
||||
" <td>7</td>\n",
|
||||
" <td>0.6</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>8</th>\n",
|
||||
" <td>8</td>\n",
|
||||
" <td>0.0</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>9</th>\n",
|
||||
" <td>9</td>\n",
|
||||
" <td>0.0</td>\n",
|
||||
" </tr>\n",
|
||||
" </tbody>\n",
|
||||
"</table>\n",
|
||||
"</div>"
|
||||
],
|
||||
"text/plain": [
|
||||
" label center_intensity\n",
|
||||
"0 0 0.0\n",
|
||||
"1 1 0.0\n",
|
||||
"2 2 0.0\n",
|
||||
"3 3 0.1\n",
|
||||
"4 4 0.0\n",
|
||||
"5 5 0.0\n",
|
||||
"6 6 0.0\n",
|
||||
"7 7 0.6\n",
|
||||
"8 8 0.0\n",
|
||||
"9 9 0.0"
|
||||
]
|
||||
},
|
||||
"execution_count": 6,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Average pixel intensity per digit\n",
|
||||
"con.execute(\"\"\"\n",
|
||||
" SELECT label,\n",
|
||||
" ROUND(AVG(px196::INT + px197::INT + px224::INT + px225::INT), 1) as center_intensity\n",
|
||||
" FROM mnist WHERE split='train'\n",
|
||||
" GROUP BY label ORDER BY label\n",
|
||||
"\"\"\").df()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Visualize Sample Digits"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"image/png": "iVBORw0KGgoAAAANSUhEUgAAA8cAAAGJCAYAAACnwkFvAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/GU6VOAAAACXBIWXMAAA9hAAAPYQGoP6dpAAA4tklEQVR4nO3dB5gUVfov4BqiYABRDBhAF1BRESPIiqBgQlYxrOiqqOuqa+TPRUx/VjHnBCpmTJjWAGbUBXNYMO1iFgUFUVGJKiDS96m+D1ylTmu3MwNMn/d9nlncH2eqDzV9pvurqv6qIpfL5RIAAACIWK2lPQEAAABY2hTHAAAARE9xDAAAQPQUxwAAAERPcQwAAED0FMcAAABET3EMAABA9BTHAAAARE9xDAAAQPQUx7/DhAkTkoqKiuSSSy6psifQM888k99m+ifUNNYEWBPgdQK8d6rpoimOb7nllnzxOXbs2KRcTZ48Odlvv/2Sxo0bJyuttFKy5557Jh9//PHSnhbLqHJfE++//37St2/fpGPHjslyyy2X/7emRTzEuiYeeOCBpFevXsn666+fNGzYMNlggw2Sfv36JdOnT1/aU2MZVe5r4sEHH0x22WWXpFmzZkn9+vWTtddeO9l3332TcePGLe2psYwq9zWxuJ122in/7z3uuOOSWNRZ2hOgasyePTvZYYcdkhkzZiSnnXZaUrdu3eTyyy9POnfunLz55pvJKqusYlcTlZdffjkZNGhQ0qZNm2SjjTbKrwOI2ZFHHpkvAg466KBk3XXXTf773/8mV111VfLYY48lr7/+etKgQYOlPUVYotI1sPLKKyd9+vRJVl111eSLL75Ibr755mSbbbbJv4ZsttlmfiJE64EHHsivg9gojsvENddck3z44YfJv//972TrrbfOZ7vttluyySabJJdeemly3nnnLe0pwhK1xx575M+IrbjiivmPQCiOid19992XdOnS5RfZlltumRxyyCHJsGHDkr/97W9LbW6wNJx++umZLF0H6RnkIUOGJNdee+1SmRcsbXPmzMlfWXTyyScH10k5i+ay6mLMmzcv/wRI3yw0atQoWX755ZNOnTolo0ePLvg96dnZ5s2b54+4p2dpQ5fivPfee/nLdJo0aZK/vHOrrbZKHnrood+cz/fff5//3q+//rqoNz1pUbywME5tuOGGSdeuXZN77733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 truncated
|
||||
"text/plain": [
|
||||
"<Figure size 1000x400 with 10 Axes>"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"fig, axes = plt.subplots(2, 5, figsize=(10, 4))\n",
|
||||
"for digit in range(10):\n",
|
||||
" row = con.execute(f\"SELECT * FROM mnist WHERE label={digit} AND split='train' LIMIT 1\").fetchone()\n",
|
||||
" pixels = np.array(row[:784]).reshape(28, 28)\n",
|
||||
" ax = axes[digit // 5, digit % 5]\n",
|
||||
" ax.imshow(pixels, cmap='gray')\n",
|
||||
" ax.set_title(f'Label: {digit}')\n",
|
||||
" ax.axis('off')\n",
|
||||
"plt.tight_layout()\n",
|
||||
"plt.show()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAbAAAAGdCAYAAABzfCbCAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/GU6VOAAAACXBIWXMAAA9hAAAPYQGoP6dpAAA8aElEQVR4nO3dCXxU1b0H8H9YEhBIICwJVAJhEZBV2UQUQSibRSK4gPQVlAcVAQVckD7ZlBahrfJQBLUWagsovMdSeIpF1qrsFi2KCIgCYlgbkGAguXPf53fsjDNhAnOSe5M5c3/fz+cSZnJy587cmfnfc87//m+cbdu2EBERGaZUSW8AERFRYTCAERGRkRjAiIjISAxgRERkJAYwIiIyEgMYEREZiQGMiIiMxABGRERGKlPSG0BERPpycnLk0qVLjrx08fHxUq5cOeN2AwMYEZGBwSs9PV0yMzMdWV9qaqocOnTIuCDGAEZEZBj0vDIzM+XIkUOSmJhYpHWdO3dOatdOV+tkACMiomKRmJhY5ABmMvbAiIiMlffvpajrMBMDGBGRsfI8HcCYRk9EREZiD4yIyFh5nu6BMYARERnLciAAYR1m4hAiEREZiT0wIiJj5XEIkYiITJTn6QDGIUQiIjIShxCJiIyV5+keGAMYEZGxLAeyCJmFSEREVKw4B0YRWbBggcTFxclXX33lyiuG9WL9eJwr2bhxo2qHn6aL9Dk75fz581KjRg1ZuHChI/u/c+fOaiksrG/KlClXfIxIOf2+mDdvnqSlpcnFixfFjPPA8oqwsAcW1V5++WX15m7fvn1JbwqVoGPHjqkvzN27d0ftfnj77bdDvtSd9N///d9SqVIlGTBggHjBokWLZNasWYX62yFDhqjLi7zyyisS3fIcWszkiR4Yjjjr1q0r27dvlwMHDpT05hjpP/7jP+T777+XOnXqlOh2dOrUSW0HfhYmgE2dOjVqAhheSzwXvLbBAQzb6LTc3FwVwP7zP/9TSpcuLbH2Hgv3vihKAMN1sQYPHizPP/+82LZdqHWQ+2I+gOEqox9++KF6I1avXr1QwydF5fP51BVUTYYvPXyo0ZMtSaVKlVLbgZ+mw2uJ51IcAWX16tVy8uRJuffeeyUW32NuvC/wWn399deyYcMGiV557IHFMgSsKlWqyB133CF33313SADDUWlycrI88MADYa9Sig/E448/HrgP4+GTJ0+WBg0aSEJCgtSuXVuefPLJy8bJ8QEcNWqUeqymTZuqtmvWrFG/+93vfic333yzVK1aVcqXLy+tW7eW//mf/7ns8XE0+cgjj0i1atXUsM+dd94p33zzzWXzBoD7H3zwQUlJSVGPhcf84x//GNHrE7ytjRo1Us8Z27R58+aQdvnnJ9avX6++LCZNmhTSDke9aDd37lxHti+SuQ7MwzRr1kw+++wz6dKli1xzzTXyk5/8RGbOnBnyd23btlX/x/7GOvLPP23btk169uwpSUlJah233XabfPDBByGPj9cef4eePIaZKleurNpjnRcuXAhpu3btWrnllltUm4oVK6rX91e/+lWBc2BY35w5cwL7xb+gB4ARhL59+172euDACI//y1/+8oqv24oVK9Q66tevH3L/J598oh63Xr16at/j0vLYV6dPnxan4PMxduxYdQDpfy8fPXr0snbh5sBw8IfXvFatWmqfYP9iP+O5YLsLel/gPfF///d/KgD5X0f8jd+LL76o3odYJ74f2rRpo967wfA5wPfDypUrJXrleTqAxXwaPb6Y+/XrJ/Hx8TJw4ED1xbpjxw71ZVa2bFm56667ZNmyZWqsG22CP/D44PnnC/BBwgfv/fffl+HDh0uTJk3kn//8p7zwwgvyxRdfqPbB8AW/ZMkSFRwQhPwfHgzjYD2DBg1SY+xvvvmm3HPPPeoIGUHWDx9O/D2GVW666SbZtGlTyO/9jh8/rn7vD0T4knjnnXdk6NChKgiPGTPmqq8R1v3WW2+pgIkAgzlDfJFjyBWBIZzbb79dHn74YZk+fbpkZGTIjTfeKN9++62MHj1aunXrJg899JBj2xeJf/3rX2qbsa9x5IyDgvHjx0vz5s2lV69ean8988wzKuBi/916663q73Aw4d9faIcvLRykIDjPnz9fPc+///3v0q5du5DHw2Okp6er5//RRx/JH/7wB5UgMWPGDPX7Tz/9VH72s59JixYt1OPidUXQyx8QgyEIYZgTge/Pf/5z4H68dj//+c9VQD5z5oz6UvVbtWqVeh3x+yvBKAT2UX54rC+//FIFYAQvbPerr76qfm7dutWRHjeGLf/yl7/I/fffr15vvNbh3svhTJgwQT3vPn36SI8ePeTjjz9WP682ovFf//VfcvbsWRUo8RkFHETAa6+9pt7rOKB99NFH1boQ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 truncated
|
||||
"text/plain": [
|
||||
"<Figure size 500x500 with 2 Axes>"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Pixel intensity heatmap averaged across all training images\n",
|
||||
"avg_pixels = con.execute(\"\"\"\n",
|
||||
" SELECT {} FROM mnist WHERE split='train'\n",
|
||||
"\"\"\".format(', '.join(f'AVG(px{i}) as px{i}' for i in range(784)))).fetchone()\n",
|
||||
"\n",
|
||||
"plt.figure(figsize=(5, 5))\n",
|
||||
"plt.imshow(np.array(avg_pixels).reshape(28, 28), cmap='hot')\n",
|
||||
"plt.title('Average pixel intensity (all digits)')\n",
|
||||
"plt.colorbar()\n",
|
||||
"plt.show()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 9,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"DuckDB connection closed. Database saved at: ..\\data\\mnist.duckdb\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"con.close()\n",
|
||||
"print('DuckDB connection closed. Database saved at:', db_path)"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "MNIST (Python)",
|
||||
"language": "python",
|
||||
"name": "mnist"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.4"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 4
|
||||
}
|
||||
@@ -0,0 +1,533 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# 02 - MNIST Model Training & Visualization\n",
|
||||
"\n",
|
||||
"This notebook trains a neural network on MNIST data loaded from DuckDB, then visualizes:\n",
|
||||
"- Training loss & accuracy curves\n",
|
||||
"- Confusion matrix\n",
|
||||
"- Per-digit accuracy\n",
|
||||
"- Misclassified examples\n",
|
||||
"\n",
|
||||
"**Key ML Concepts Covered:**\n",
|
||||
"- Neural network architecture (layers, activations)\n",
|
||||
"- Forward/backward propagation\n",
|
||||
"- Loss functions & optimizers\n",
|
||||
"- Batch training with DataLoaders\n",
|
||||
"- Overfitting detection (train vs validation)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import numpy as np\n",
|
||||
"import duckdb\n",
|
||||
"import torch\n",
|
||||
"import torch.nn as nn\n",
|
||||
"from torch.utils.data import DataLoader, TensorDataset\n",
|
||||
"import matplotlib.pyplot as plt\n",
|
||||
"import seaborn as sns\n",
|
||||
"from sklearn.metrics import confusion_matrix, classification_report\n",
|
||||
"from pathlib import Path\n",
|
||||
"from tqdm.notebook import tqdm"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 1. Load Data from DuckDB"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Train: (60000, 784), Test: (10000, 784)\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"con = duckdb.connect(str(Path('../data/mnist.duckdb')), read_only=True)\n",
|
||||
"\n",
|
||||
"train_data = con.execute(\"SELECT * FROM mnist WHERE split='train'\").fetchnumpy()\n",
|
||||
"test_data = con.execute(\"SELECT * FROM mnist WHERE split='test'\").fetchnumpy()\n",
|
||||
"con.close()\n",
|
||||
"\n",
|
||||
"# Extract pixel columns and labels\n",
|
||||
"px_cols = [f'px{i}' for i in range(784)]\n",
|
||||
"X_train = np.column_stack([train_data[c] for c in px_cols]).astype(np.float32) / 255.0\n",
|
||||
"y_train = train_data['label'].astype(np.int64)\n",
|
||||
"X_test = np.column_stack([test_data[c] for c in px_cols]).astype(np.float32) / 255.0\n",
|
||||
"y_test = test_data['label'].astype(np.int64)\n",
|
||||
"\n",
|
||||
"print(f'Train: {X_train.shape}, Test: {X_test.shape}')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 2. Create PyTorch DataLoaders\n",
|
||||
"\n",
|
||||
"DataLoaders handle batching and shuffling. Batch size is a key hyperparameter:\n",
|
||||
"- **Smaller batches** → noisier gradients, can help escape local minima\n",
|
||||
"- **Larger batches** → smoother gradients, faster training per epoch"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"BATCH_SIZE = 32\n",
|
||||
"\n",
|
||||
"train_ds = TensorDataset(torch.tensor(X_train), torch.tensor(y_train))\n",
|
||||
"test_ds = TensorDataset(torch.tensor(X_test), torch.tensor(y_test))\n",
|
||||
"\n",
|
||||
"train_loader = DataLoader(train_ds, batch_size=BATCH_SIZE, shuffle=True)\n",
|
||||
"test_loader = DataLoader(test_ds, batch_size=BATCH_SIZE)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 3. Define the Neural Network\n",
|
||||
"\n",
|
||||
"Architecture: 784 → 256 → 128 → 10\n",
|
||||
"\n",
|
||||
"**Key parameters to experiment with:**\n",
|
||||
"- Hidden layer sizes (try 64, 128, 256, 512)\n",
|
||||
"- Number of layers (try 1-4 hidden layers)\n",
|
||||
"- Activation function (ReLU, Sigmoid, Tanh)\n",
|
||||
"- Dropout rate (regularization to prevent overfitting)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Using device: cpu\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"class MNISTNet(nn.Module):\n",
|
||||
" def __init__(self, hidden1=256, hidden2=128, dropout=0.2):\n",
|
||||
" super().__init__()\n",
|
||||
" self.net = nn.Sequential(\n",
|
||||
" nn.Linear(784, hidden1),\n",
|
||||
" nn.ReLU(),\n",
|
||||
" nn.Dropout(dropout),\n",
|
||||
" nn.Linear(hidden1, hidden2),\n",
|
||||
" nn.ReLU(),\n",
|
||||
" nn.Dropout(dropout),\n",
|
||||
" nn.Linear(hidden2, 10)\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
" def forward(self, x):\n",
|
||||
" return self.net(x)\n",
|
||||
"\n",
|
||||
"device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n",
|
||||
"print(f'Using device: {device}')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 4. Training Loop\n",
|
||||
"\n",
|
||||
"**Hyperparameters:**\n",
|
||||
"- `lr` (learning rate): How big each optimization step is. Too high → diverges. Too low → slow.\n",
|
||||
"- `epochs`: Number of full passes through the training data.\n",
|
||||
"- `optimizer`: Adam adapts learning rate per-parameter. SGD is simpler but needs tuning."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Model parameters: 235,146\n",
|
||||
"MNISTNet(\n",
|
||||
" (net): Sequential(\n",
|
||||
" (0): Linear(in_features=784, out_features=256, bias=True)\n",
|
||||
" (1): ReLU()\n",
|
||||
" (2): Dropout(p=0.2, inplace=False)\n",
|
||||
" (3): Linear(in_features=256, out_features=128, bias=True)\n",
|
||||
" (4): ReLU()\n",
|
||||
" (5): Dropout(p=0.2, inplace=False)\n",
|
||||
" (6): Linear(in_features=128, out_features=10, bias=True)\n",
|
||||
" )\n",
|
||||
")\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# === HYPERPARAMETERS (experiment with these!) ===\n",
|
||||
"EPOCHS = 15\n",
|
||||
"LEARNING_RATE = 1e-3\n",
|
||||
"HIDDEN1 = 256\n",
|
||||
"HIDDEN2 = 128\n",
|
||||
"DROPOUT = 0.2\n",
|
||||
"\n",
|
||||
"model = MNISTNet(HIDDEN1, HIDDEN2, DROPOUT).to(device)\n",
|
||||
"criterion = nn.CrossEntropyLoss()\n",
|
||||
"optimizer = torch.optim.Adam(model.parameters(), lr=LEARNING_RATE)\n",
|
||||
"\n",
|
||||
"print(f'Model parameters: {sum(p.numel() for p in model.parameters()):,}')\n",
|
||||
"print(model)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"application/vnd.jupyter.widget-view+json": {
|
||||
"model_id": "40c8fa44b05646f6a477aa9682899b80",
|
||||
"version_major": 2,
|
||||
"version_minor": 0
|
||||
},
|
||||
"text/plain": [
|
||||
"Training: 0%| | 0/15 [00:00<?, ?it/s]"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Epoch 1 | Train Loss: 0.3803 Acc: 0.8898 | Test Loss: 0.1511 Acc: 0.9547\n",
|
||||
"Epoch 2 | Train Loss: 0.1498 Acc: 0.9547 | Test Loss: 0.1011 Acc: 0.9693\n",
|
||||
"Epoch 3 | Train Loss: 0.1079 Acc: 0.9671 | Test Loss: 0.0837 Acc: 0.9731\n",
|
||||
"Epoch 4 | Train Loss: 0.0836 Acc: 0.9739 | Test Loss: 0.0759 Acc: 0.9762\n",
|
||||
"Epoch 5 | Train Loss: 0.0703 Acc: 0.9781 | Test Loss: 0.0729 Acc: 0.9775\n",
|
||||
"Epoch 6 | Train Loss: 0.0585 Acc: 0.9814 | Test Loss: 0.0722 Acc: 0.9782\n",
|
||||
"Epoch 7 | Train Loss: 0.0532 Acc: 0.9825 | Test Loss: 0.0709 Acc: 0.9804\n",
|
||||
"Epoch 8 | Train Loss: 0.0444 Acc: 0.9861 | Test Loss: 0.0724 Acc: 0.9783\n",
|
||||
"Epoch 9 | Train Loss: 0.0422 Acc: 0.9861 | Test Loss: 0.0615 Acc: 0.9816\n",
|
||||
"Epoch 10 | Train Loss: 0.0350 Acc: 0.9888 | Test Loss: 0.0631 Acc: 0.9828\n",
|
||||
"Epoch 11 | Train Loss: 0.0358 Acc: 0.9887 | Test Loss: 0.0622 Acc: 0.9821\n",
|
||||
"Epoch 12 | Train Loss: 0.0307 Acc: 0.9894 | Test Loss: 0.0670 Acc: 0.9817\n",
|
||||
"Epoch 13 | Train Loss: 0.0294 Acc: 0.9904 | Test Loss: 0.0677 Acc: 0.9827\n",
|
||||
"Epoch 14 | Train Loss: 0.0264 Acc: 0.9906 | Test Loss: 0.0659 Acc: 0.9818\n",
|
||||
"Epoch 15 | Train Loss: 0.0251 Acc: 0.9916 | Test Loss: 0.0653 Acc: 0.9826\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Training with metrics tracking\n",
|
||||
"history = {'train_loss': [], 'train_acc': [], 'test_loss': [], 'test_acc': []}\n",
|
||||
"\n",
|
||||
"for epoch in tqdm(range(EPOCHS), desc='Training'):\n",
|
||||
" # Train\n",
|
||||
" model.train()\n",
|
||||
" running_loss, correct, total = 0.0, 0, 0\n",
|
||||
" for X_batch, y_batch in train_loader:\n",
|
||||
" X_batch, y_batch = X_batch.to(device), y_batch.to(device)\n",
|
||||
" optimizer.zero_grad()\n",
|
||||
" outputs = model(X_batch)\n",
|
||||
" loss = criterion(outputs, y_batch)\n",
|
||||
" loss.backward()\n",
|
||||
" optimizer.step()\n",
|
||||
" running_loss += loss.item() * len(y_batch)\n",
|
||||
" correct += (outputs.argmax(1) == y_batch).sum().item()\n",
|
||||
" total += len(y_batch)\n",
|
||||
" history['train_loss'].append(running_loss / total)\n",
|
||||
" history['train_acc'].append(correct / total)\n",
|
||||
"\n",
|
||||
" # Evaluate\n",
|
||||
" model.eval()\n",
|
||||
" running_loss, correct, total = 0.0, 0, 0\n",
|
||||
" with torch.no_grad():\n",
|
||||
" for X_batch, y_batch in test_loader:\n",
|
||||
" X_batch, y_batch = X_batch.to(device), y_batch.to(device)\n",
|
||||
" outputs = model(X_batch)\n",
|
||||
" loss = criterion(outputs, y_batch)\n",
|
||||
" running_loss += loss.item() * len(y_batch)\n",
|
||||
" correct += (outputs.argmax(1) == y_batch).sum().item()\n",
|
||||
" total += len(y_batch)\n",
|
||||
" history['test_loss'].append(running_loss / total)\n",
|
||||
" history['test_acc'].append(correct / total)\n",
|
||||
"\n",
|
||||
" print(f\"Epoch {epoch+1:2d} | Train Loss: {history['train_loss'][-1]:.4f} Acc: {history['train_acc'][-1]:.4f} | \"\n",
|
||||
" f\"Test Loss: {history['test_loss'][-1]:.4f} Acc: {history['test_acc'][-1]:.4f}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 5. Training Curves\n",
|
||||
"\n",
|
||||
"**What to look for:**\n",
|
||||
"- If test loss increases while train loss decreases → **overfitting** (try more dropout, fewer parameters)\n",
|
||||
"- If both losses plateau high → **underfitting** (try more parameters, lower dropout)\n",
|
||||
"- Gap between train/test accuracy shows generalization"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"image/png": "iVBORw0KGgoAAAANSUhEUgAABKUAAAGGCAYAAACqvTJ0AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/GU6VOAAAACXBIWXMAAA9hAAAPYQGoP6dpAACjbElEQVR4nOzdB3hUVdoH8P9Mkpn03qkJBEIHQUBFsaAoNqyArii6WFZcWb5dK2IXK4uFFUVRFkVxFV3XgiKKiNIEESGEXtMTSO8z8z3vmcxk0iCBZO6U/8/nOnfu3NzcnEzuHN77nvfoLBaLBURERERERERERE6kd+Y3IyIiIiIiIiIiEgxKERERERERERGR0zEoRURERERERERETsegFBEREREREREROR2DUkRERERERERE5HQMShERERERERERkdMxKEVERERERERERE7HoBQRERERERERETkdg1JEREREREREROR0DEoREREREREREZHTMShFRE7z7rvvQqfT4ddff3WLVt+yZQv+9Kc/oUuXLjAajYiMjMSYMWPwzjvvwGQyaX16RERE5Ab+9a9/qf7PiBEjtD4Vt5STk4O///3vSE1NRWBgIIKCgjB06FA89dRTKCws1Pr0iOgU+Z7qAYiIPNFbb72FO++8E3FxcbjpppuQkpKCkpISrFy5ErfddhuysrLw0EMPaX2aRERE5OLef/99dO/eHRs2bMCePXvQs2dPrU/JbWzcuBHjxo1DaWmpulEowSghNzifffZZrF69Gt9++63Wp0lEp4BBKSKiRtatW6cCUmeccQa++uorhISE2F+bPn266ght27atXdqtrKxM3fEjIiIiz7N//3788ssvWLZsGe644w4VoHr00UfhilytTyJZUFdddRV8fHzw22+/qUwpR08//TQWLFjgkT87kTfh8D0icjnS8bjkkksQGhqK4OBgXHDBBSpQ5KimpgaPP/64ymDy9/dHVFQURo0ahRUrVtj3yc7OxpQpU9C5c2c1/C4hIQFXXnklDhw4cNzvL8eVNHvpODoGpGyGDRuGW265Ra2vWrVK7SuPjuR7yHYZsmgjXyM/z969e9VdPzn2jTfeiGnTpqnt5eXlTb7XpEmTEB8f32C44Ndff42zzz5bdZ7kGJdeeim2b9/eqrYlIiIi55G+REREhPqsvvbaa9XzlgIwf/vb31RGlfRZpO8y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 truncated
|
||||
"text/plain": [
|
||||
"<Figure size 1200x400 with 2 Axes>"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))\n",
|
||||
"\n",
|
||||
"ax1.plot(history['train_loss'], label='Train')\n",
|
||||
"ax1.plot(history['test_loss'], label='Test')\n",
|
||||
"ax1.set_xlabel('Epoch')\n",
|
||||
"ax1.set_ylabel('Loss')\n",
|
||||
"ax1.set_title('Loss Curve')\n",
|
||||
"ax1.legend()\n",
|
||||
"ax1.grid(True, alpha=0.3)\n",
|
||||
"\n",
|
||||
"ax2.plot(history['train_acc'], label='Train')\n",
|
||||
"ax2.plot(history['test_acc'], label='Test')\n",
|
||||
"ax2.set_xlabel('Epoch')\n",
|
||||
"ax2.set_ylabel('Accuracy')\n",
|
||||
"ax2.set_title('Accuracy Curve')\n",
|
||||
"ax2.legend()\n",
|
||||
"ax2.grid(True, alpha=0.3)\n",
|
||||
"\n",
|
||||
"plt.tight_layout()\n",
|
||||
"plt.show()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 6. Confusion Matrix\n",
|
||||
"\n",
|
||||
"Shows which digits get confused with each other (e.g., 4↔9, 3↔5 are common)."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"image/png": "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 truncated
|
||||
"text/plain": [
|
||||
"<Figure size 800x600 with 2 Axes>"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"model.eval()\n",
|
||||
"all_preds, all_labels = [], []\n",
|
||||
"with torch.no_grad():\n",
|
||||
" for X_batch, y_batch in test_loader:\n",
|
||||
" preds = model(X_batch.to(device)).argmax(1).cpu()\n",
|
||||
" all_preds.append(preds)\n",
|
||||
" all_labels.append(y_batch)\n",
|
||||
"\n",
|
||||
"all_preds = torch.cat(all_preds).numpy()\n",
|
||||
"all_labels = torch.cat(all_labels).numpy()\n",
|
||||
"\n",
|
||||
"cm = confusion_matrix(all_labels, all_preds)\n",
|
||||
"plt.figure(figsize=(8, 6))\n",
|
||||
"sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',\n",
|
||||
" xticklabels=range(10), yticklabels=range(10))\n",
|
||||
"plt.xlabel('Predicted')\n",
|
||||
"plt.ylabel('Actual')\n",
|
||||
"plt.title('Confusion Matrix')\n",
|
||||
"plt.show()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 9,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
" precision recall f1-score support\n",
|
||||
"\n",
|
||||
" 0 0.9798 0.9908 0.9853 980\n",
|
||||
" 1 0.9877 0.9930 0.9903 1135\n",
|
||||
" 2 0.9872 0.9700 0.9785 1032\n",
|
||||
" 3 0.9861 0.9832 0.9846 1010\n",
|
||||
" 4 0.9825 0.9725 0.9775 982\n",
|
||||
" 5 0.9898 0.9798 0.9848 892\n",
|
||||
" 6 0.9864 0.9875 0.9870 958\n",
|
||||
" 7 0.9806 0.9815 0.9810 1028\n",
|
||||
" 8 0.9785 0.9805 0.9795 974\n",
|
||||
" 9 0.9679 0.9861 0.9769 1009\n",
|
||||
"\n",
|
||||
" accuracy 0.9826 10000\n",
|
||||
" macro avg 0.9827 0.9825 0.9825 10000\n",
|
||||
"weighted avg 0.9826 0.9826 0.9826 10000\n",
|
||||
"\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"print(classification_report(all_labels, all_preds, digits=4))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 7. Misclassified Examples\n",
|
||||
"\n",
|
||||
"Seeing what the model gets wrong helps understand its weaknesses."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 10,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"image/png": "iVBORw0KGgoAAAANSUhEUgAABHoAAAHuCAYAAADgAGuHAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/GU6VOAAAACXBIWXMAAA9hAAAPYQGoP6dpAABVF0lEQVR4nO3dB7gU1fk44HMpKvaKigo2MJbYwF7R2IItscQWK3ZR7EosMZbYW+xGsZdYsUSNGtvPGrBrjB1FwYpRVEBh/8/Z+d8Alxm4c9nL7s593+e5D/DNzsy5u3uY2W/POV9DqVQqBQAAAADqXrtqNwAAAACAypDoAQAAACgIiR4AAACAgpDoAQAAACgIiR4AAACAgpDoAQAAACgIiR4AAACAgpDoAQAAACgIiR4AAACAgpDoAYDp7JprrgkNDQ3lP1tbPM/6668f6uX3v/nmm8PKK68cZptttvJj+vfvX44vuuii5Z9qtIkpa+3XBgDIR6IHAHL48MMPywmB+LPAAguEn3/+OfVx//73v//3OB+Cm+fZZ58NO++8c/j222/D/vvvH0488cSw6aab1uT7MybPGl/frJ/HH3+82s0EANqgDtVuAADUow4dOoTPPvss/P3vfw9bbrnlZNuvuuqq0K5d+vcpv/nNb8Lqq68eFlxwwdAWZf3+999/fyiVSuG6664La6655iTbHn300VCLDj/88DDrrLOmbpPgAwCqQaIHAFogJiJeeeWVcPXVV0+W6ImjfG644Ybwq1/9KjzxxBOT7TvHHHOUf9qqrN//008/Lf/ZpUuXybYtscQSoRYdccQR5ZFdAAC1wtQtAGiBTp06hR122KE8CuXzzz+fZNt9991XHu2z55575loP5sUXXwzbbrtt6Nq1a5hxxhnDfPPNF1ZZZZVw6qmnTnaMeM44mmSppZYqt2XuuecOq622Wjj77LOn2va33347HHXUUeW1cOaZZ54w00wzhR49eoRjjjkmjBo1arLHDx8+PBxyyCGhe/fu5XPNOeecYemllw777bdf+O9///u/x8W/n3DCCWGZZZYpj3KZffbZw5JLLhl22223MHTo0MzfP05xiv8eOHBg+d+LLbbY/6Y/xalyU1oHJo4Aism2tdZaq3y+mWeeOfTq1ascS/P111+X2z3//POXHxuf37vuuiu0pnfffbe85tDCCy8cvvrqq2Zti+3faqutyr9zfH3i67vJJpuExx57bLLjNz5/f/zjH8MzzzwTevfuXT5mfP8ccMAB4ccffyw/Lr5X11hjjTDLLLOUf//4Hmg69XDi12bQoEFh1VVXLT9P8Vjx/Rzf182V57UZPXp0OOecc8IKK6xQTgLGNsbfffvtty8nVAGA5pPoAYAWih984wfl66+/fpJ4/CAbP5hvvfXWzT7Wyy+/XB4l9MADD4S11147HHbYYeWkT/xwfMUVV0zy2P/85z9hxRVXDOeee27o3LlzOPjgg8NOO+1Ufuxpp5021XPdeeed5alliy++eDkJExMfsb1nnHFG2GijjcJPP/30v8f+8MMP5Q/qf/nLX8qjavr16xd23333cmIo/t5ffPHF/z7Ux0TEySefXD7WPvvsU/5ZaaWVwj333BPeeeedzPbED/RxPZ74IT+KSaX47/gTk0pZ4jnjmj577bVXuR3xOejbt2/4/vvvy7E42mZi8XeJa+tcfvnl5d8lnicmyn73u9+F22+/PbSWmOyKz98nn3xSbl+j+DzvuOOO5XbF5zIm3RodeOCB5aRKHBV26KGHhs0337y8hlH8d0zApHn++efDhhtuWE6U7LvvvuWE4aWXXhr23nvvcOutt5bfT926dStvi8/rWWedlfl+ueOOO8J2221XbntcEPuXv/xlOREX35sjR46c6u+c97WJ78PG2B577BEOOuigcn946qmnwr/+9a9mP9cAQHIhBgCa6YMPPijFy+cmm2xS/vdyyy1XWnbZZf+3ffjw4aUOHTqU+vXrV/73jDPOWOrWrdskxxg4cGD5GPHPRocddlg5dvfdd092zi+//HKSf/fq1av82CuuuGKyx3788ceT/Ds+br311pskNmzYsNKYMWMm2/ekk04qP/6GG274X+yee+4px/r37z/Z47/77rvS6NGjy39/9dVXy4/beuutJ3tcfEx87JR+/2i33XYrx+Nz3FR8Dps+j/H3j4/fY489SmPHjv1fPP5uW2yxRXnb4MGD/xc/8cQTy7G99957kuM8+OCD5Xham7LE5zQ+/vDDDy8ft+nPn//858n22WGHHcr7XHLJJeV/H3nkkeV/H3vssZM99v33358s9umnn5a6dOlS6t69+yTxxx577H/tn/j9E5+T5ZdfvtTQ0FCad955Sy+88ML/tn377belzp07l+aee+5JnrvG1yb+xOdl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 truncated
|
||||
"text/plain": [
|
||||
"<Figure size 1200x500 with 10 Axes>"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"wrong_idx = np.where(all_preds != all_labels)[0][:10]\n",
|
||||
"\n",
|
||||
"fig, axes = plt.subplots(2, 5, figsize=(12, 5))\n",
|
||||
"for i, idx in enumerate(wrong_idx):\n",
|
||||
" ax = axes[i // 5, i % 5]\n",
|
||||
" ax.imshow(X_test[idx].reshape(28, 28), cmap='gray')\n",
|
||||
" ax.set_title(f'True: {all_labels[idx]}\\nPred: {all_preds[idx]}', color='red')\n",
|
||||
" ax.axis('off')\n",
|
||||
"plt.suptitle('Misclassified Examples', fontsize=14)\n",
|
||||
"plt.tight_layout()\n",
|
||||
"plt.show()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 8. Save Model & Training Results to DuckDB"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 11,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Model saved. Final test accuracy: 0.9826\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"import pandas as pd\n",
|
||||
"\n",
|
||||
"# Save training history\n",
|
||||
"history_df = pd.DataFrame(history)\n",
|
||||
"history_df['epoch'] = range(1, EPOCHS + 1)\n",
|
||||
"\n",
|
||||
"con = duckdb.connect(str(Path('../data/mnist.duckdb')))\n",
|
||||
"con.execute('DROP TABLE IF EXISTS training_history')\n",
|
||||
"con.execute('CREATE TABLE training_history AS SELECT * FROM history_df')\n",
|
||||
"con.close()\n",
|
||||
"\n",
|
||||
"# Save model weights\n",
|
||||
"torch.save(model.state_dict(), '../data/mnist_model.pth')\n",
|
||||
"print(f'Model saved. Final test accuracy: {history[\"test_acc\"][-1]:.4f}')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 9. Experiment Ideas\n",
|
||||
"\n",
|
||||
"Try changing these parameters and re-running to see the effect:\n",
|
||||
"\n",
|
||||
"| Parameter | Current | Try |\n",
|
||||
"|-----------|---------|-----|\n",
|
||||
"| `LEARNING_RATE` | 0.001 | 0.01, 0.0001 |\n",
|
||||
"| `HIDDEN1` | 256 | 64, 512 |\n",
|
||||
"| `HIDDEN2` | 128 | 32, 256 |\n",
|
||||
"| `DROPOUT` | 0.2 | 0.0, 0.5 |\n",
|
||||
"| `EPOCHS` | 15 | 5, 30 |\n",
|
||||
"| `BATCH_SIZE` | 128 | 32, 512 |\n",
|
||||
"| Optimizer | Adam | SGD(lr=0.01, momentum=0.9) |"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "MNIST (Python)",
|
||||
"language": "python",
|
||||
"name": "mnist"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.4"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 4
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
# Jupyter & VSCode notebook support
|
||||
jupyter==1.1.1
|
||||
ipykernel==6.29.5
|
||||
ipywidgets==8.1.5
|
||||
|
||||
# Data & Database
|
||||
numpy==1.26.4
|
||||
pandas==2.2.3
|
||||
duckdb==1.5.2
|
||||
|
||||
# ML Framework
|
||||
torch==2.5.1
|
||||
torchvision==0.20.1
|
||||
|
||||
# Visualization
|
||||
matplotlib==3.9.3
|
||||
seaborn==0.13.2
|
||||
tqdm==4.67.1
|
||||
|
||||
# Utilities
|
||||
scikit-learn==1.6.0
|
||||
Reference in new issue
Block a user