Convolutional neural networks implemented from scratch with NumPy — no PyTorch, TensorFlow, or autograd engine. Every layer, loss, optimiser and augmentation is written by hand with explicit forward/backward passes, then trained on the Durian Leaf Diseases image-classification dataset.
The goal of this project is to make the mathematics behind CNNs concrete: you can read exactly what a convolution, a pooling layer or Adam does, line by line.
| Area | Components |
|---|---|
| Layers | Conv2D (im2col), MaxPool2D, GlobalAvgPool2D, Dense, Flatten, BatchNorm2D, Dropout |
| Activations | ReLU, LeakyReLU, Sigmoid, Tanh, Softmax |
| Losses | CrossEntropyLoss, MSELoss |
| Optimisers | SGD (momentum + weight decay), Adam |
| LR schedules | StepLR, CosineAnnealingLR |
| Augmentation | horizontal flip, random crop, 90° rotation, brightness |
| Training | fit() / evaluate(), early stopping, best-weight restore, hflip TTA, multi-seed ensembling |
| Model | Sequential container with parameter summary |
A Durian Leaf Image Dataset of Common Diseases in Vietnam for Agricultural Diagnosis — 6 classes of durian leaf conditions:
Leaf_Algal Leaf_Blight Leaf_Colletotrichum
Leaf_Healthy Leaf_Phomopsis Leaf_Rhizoctonia
The dataset is committed in this repository under
data/Durian_Leaf_Diseases/, split as:
| split | images |
|---|---|
| train | 1814 |
| val | 387 |
| test | 394 |
Layout expected by the loader:
data/Durian_Leaf_Diseases/<split>/<class>/*.jpg
Images are decoded with Pillow, resized, scaled to [0, 1] and standardised
using the per-channel mean/std computed on the train split.
uv— the only thing you need to install. All Python dependencies are pinned inpyproject.toml/uv.lock.- Python 3.14+. You do not need to install Python yourself:
uvreads.python-versionand downloads a matching interpreter automatically.
# 1. Install uv (skip if you already have it)
curl -LsSf https://astral.sh/uv/install.sh | sh
# 2. Get the code
git clone https://github.com/nvtoan0201-swe/neuroforge.git
cd neuroforge
# 3. Create the virtualenv and install dependencies
uv syncThat's it — the dataset is committed in the repo, so you can train right away:
uv run train_cnn.py --image-size 32 --blocks 2 --epochs 1 # quick smoke testTrain the CNN (defaults: 30 epochs, image size 64, Adam):
uv run train_cnn.py --image-size 64 --batch-size 32Best configuration found so far (96×96, gap head, batch norm, cosine LR, early stopping, TTA):
uv run train_cnn.py \
--image-size 96 --batch-size 32 --epochs 30 \
--optimizer adam --lr 1e-3 --lr-schedule cosine \
--width 32 --blocks 3 --pool gap --batchnorm \
--augment --crop-pad 4 --patience 8 --ttaMulti-seed ensemble:
uv run train_cnn.py --image-size 64 --augment --batchnorm \
--seeds 0,1,2 --tta --run-name ensemble_demo| Flag | Default | Description |
|---|---|---|
--data-root |
data/Durian_Leaf_Diseases |
Dataset root |
--epochs |
30 |
Max epochs |
--batch-size |
32 |
Mini-batch size |
--image-size |
64 |
Square input size; must be divisible by 2**blocks |
--seed |
0 |
Random seed |
--optimizer |
adam |
adam or sgd |
--lr |
1e-3 |
Learning rate |
--momentum |
0.9 |
SGD momentum |
--weight-decay |
0.0 |
L2 weight decay |
--beta1 / --beta2 / --eps |
0.9 / 0.999 / 1e-8 |
Adam hyper-parameters |
--lr-schedule |
none |
none, step or cosine |
--min-lr |
0.0 |
Cosine eta_min |
--step-size / --gamma |
10 / 0.1 |
StepLR parameters |
--width |
16 |
Base number of conv channels (doubles each block) |
--blocks |
2 |
Number of conv blocks |
--pool |
gap |
Classifier head: gap or flatten |
--dropout |
0.0 |
Dropout probability |
--batchnorm |
off | Add BatchNorm2D after each conv |
--augment |
off | Horizontal flip + random crop |
--crop-pad |
4 |
Crop padding when augmenting |
--rotate90 |
off | Add random 90° rotations |
--brightness |
0.0 |
Random brightness delta |
--patience |
0 |
Early-stopping patience (0 = off) |
--min-delta |
0.0 |
Minimum monitored improvement |
--early-stop-metric |
loss |
loss or acc |
--no-restore-best |
off | Keep final weights instead of best |
--tta |
off | Horizontal-flip test-time augmentation |
--seeds |
— | Comma-separated seeds for ensembling, e.g. 0,1,2 |
--run-name |
auto | Report file name |
--runs-dir |
runs |
Directory for reports |
--output |
— | Explicit report path |
Every run writes a Markdown report (config, model summary, per-epoch history,
test results) to runs/<run-name>.md and appends a row to
runs/summary.md.
Test accuracy on the held-out test split (394 images). The test set is evaluated once after training, never used to update weights.
| run | image | head | width | blocks | dropout | test acc |
|---|---|---|---|---|---|---|
s96_gap_w32b3 |
96 | gap | 32 | 3 | 0.0 | 0.8528 |
s96_gap_w32b3_rot |
96 | gap | 32 | 3 | 0.0 | 0.8426 |
s96_gap_w32b3_bri |
96 | gap | 32 | 3 | 0.0 | 0.8325 |
s64_gap_w32b3 |
64 | gap | 32 | 3 | 0.0 | 0.8249 |
s32_gap_w32b3 |
32 | gap | 32 | 3 | 0.0 | 0.7513 |
s32_flat_w16b2 |
32 | flatten | 16 | 2 | 0.0 | 0.7030 |
s96_gap_w32b3_do0.3 |
96 | gap | 32 | 3 | 0.3 | 0.6827 |
s96_gap_w32b3_full |
96 | gap | 32 | 3 | 0.3 | 0.6294 |
s32_gap_w16b2 |
32 | gap | 16 | 2 | 0.0 | 0.5812 |
Key findings:
- Input resolution matters most: 32 → 64 → 96 monotonically improves the
gap/w32/b3model (0.7513 → 0.8249 → 0.8528). - Global average pooling beats flatten with far fewer parameters (0.7513 vs 0.7030 at 32×32).
- Dropout 0.3 hurts this small model at 96×96 (0.8528 → 0.6827); extra rotation/brightness augmentation also gives no gain over plain hflip + crop.
- An earlier optimizer sweep found
Adam lr=1e-3clearly ahead oflr=1e-2and SGD variants on the original 32×32 flatten model.
Full per-epoch logs live in runs/; the table index is
runs/summary.md.
src/neuroforge/
activations.py ReLU, LeakyReLU, Sigmoid, Tanh, Softmax
layers.py Conv2D (im2col), MaxPool2D, Dense, Flatten,
GlobalAvgPool2D, BatchNorm2D, Dropout
losses.py CrossEntropyLoss, MSELoss
optimizers.py SGD (momentum), Adam, StepLR, CosineAnnealingLR
model.py Sequential container + accuracy
data.py ImageDataset loader + batching
augment.py NumPy image augmentations
training.py fit() / evaluate() loops
train_cnn.py training entry point + experiment reports
runs/ per-experiment Markdown reports + summary.md
data/ Durian Leaf Diseases dataset
uv run ruff check . # lint- Everything runs on CPU with NumPy. There is no CUDA/GPU backend, so training time scales with image resolution.
--image-sizemust be divisible by2**blocks(two 2×2 pools per block by default) or the script exits with an error.