Tutorial 1 — Your First Network¶
This tutorial builds a neural network from scratch, step by step. No assumptions about prior ML experience.
What is a neural network?¶
A neural network is a function that learns to map inputs to outputs by adjusting its internal parameters (called weights) based on examples.
It is made of layers. Each layer takes a batch of numbers in and produces a batch of numbers out. Stack several layers together and you have a network.
Input → [Layer 1] → [Layer 2] → [Layer 3] → Output
(learn) (learn) (learn)
The problem: XOR¶
XOR is the classic “hello world” for neural nets. It cannot be solved by a single straight line (it is not linearly separable), so a network needs at least one hidden layer.
x₁ |
x₂ |
x₁ XOR x₂ |
|---|---|---|
0 |
0 |
0 |
0 |
1 |
1 |
1 |
0 |
1 |
1 |
1 |
0 |
Step 1 — Create the data¶
import numpy as np
X = np.array([[0, 0],
[0, 1],
[1, 0],
[1, 1]], dtype=np.float64)
y = np.array([[0], [1], [1], [0]], dtype=np.float64)
X has shape (4, 2) — four samples, two features each.
y has shape (4, 1) — one binary target per sample.
Step 2 — Build the model¶
from snn.model import Sequential
from snn.layers import Dense
model = Sequential([
Dense(8, activation="tanh"), # hidden layer: 2 → 8 neurons
Dense(4, activation="tanh"), # hidden layer: 8 → 4 neurons
Dense(1, activation="sigmoid"),# output: probability in (0, 1)
])
Dense layer — the most basic layer. Every input neuron connects to every output neuron. The computation is:
output = activation(input @ W + b)
where W is a weight matrix and b is a bias vector, both learned during
training.
Activation function — applies a non-linear transformation element-wise. Without activations, stacking Dense layers is identical to a single layer (pure matrix multiplication is linear). The activations are what let the network learn curved, non-linear patterns.
"tanh"→ output in (−1, 1), smooth, good for hidden layers"sigmoid"→ output in (0, 1), good for binary output probabilities
Step 3 — Compile¶
model.compile(
optimizer="adam",
loss="binary_crossentropy",
metrics=["binary_accuracy"],
)
Optimizer — the algorithm that adjusts weights after each batch. Adam is a safe default: it adapts the learning rate per parameter.
Loss — the function that measures how wrong the predictions are. Binary crossentropy is the standard loss for binary (0/1) problems.
Metrics — extra things to track during training (not used to update weights, just for monitoring).
Step 4 — Train¶
history = model.fit(
X, y,
epochs=1000,
batch_size=4, # all 4 samples at once (dataset is tiny)
verbose=0, # silent — we'll print manually
)
print(f"Final loss: {history['loss'][-1]:.4f}")
print(f"Final accuracy: {history['binary_accuracy'][-1]:.4f}")
Each epoch is one full pass over the training data:
Forward pass — predict outputs from current weights
Compute loss
Backward pass — compute how much each weight contributed to the loss
Optimizer step — nudge every weight in the direction that reduces the loss
After 1000 epochs you should see loss < 0.02 and accuracy = 1.0.
Step 5 — Inspect predictions¶
preds = model.predict(X)
for xi, yi, pi in zip(X, y.flatten(), preds.flatten()):
print(f" {xi} → target={int(yi)} predicted={pi:.4f} "
f"(rounded: {int(round(pi))})")
Expected output:
[0. 0.] → target=0 predicted=0.0089 (rounded: 0)
[0. 1.] → target=1 predicted=0.9921 (rounded: 1)
[1. 0.] → target=1 predicted=0.9935 (rounded: 1)
[1. 1.] → target=0 predicted=0.0143 (rounded: 0)
Step 6 — Model summary¶
model.summary()
=================================================================
Layer Output Shape Params
=================================================================
dense_0 ? 24
dense_1 ? 36
dense_2 ? 5
=================================================================
Total params: 65
=================================================================
Param count for a Dense layer: (input_size + 1) × output_size
(the + 1 is the bias vector).
Full code¶
import numpy as np
from snn.model import Sequential
from snn.layers import Dense
# Data
X = np.array([[0,0],[0,1],[1,0],[1,1]], dtype=np.float64)
y = np.array([[0],[1],[1],[0]], dtype=np.float64)
# Model
model = Sequential([
Dense(8, activation="tanh"),
Dense(4, activation="tanh"),
Dense(1, activation="sigmoid"),
])
model.compile("adam", "binary_crossentropy", metrics=["binary_accuracy"])
model.fit(X, y, epochs=1000, batch_size=4, verbose=0)
# Results
preds = model.predict(X)
print(preds.round(3))
# [[0.009], [0.992], [0.993], [0.014]]
What next?¶
Tutorial 2 — multi-class classification with real data, train/test splits, and batch normalisation.
Tutorial 3 — predicting continuous values and learning non-linear functions like y = x².