Tutorial 3 — Regression¶
Regression means predicting a continuous number rather than a class label. This tutorial covers linear regression, polynomial regression, and learning the non-linear function y = x².
When to use regression¶
Problem |
Loss |
Output activation |
|---|---|---|
Predict a price, age, or score |
|
None (Linear) |
Predict when outliers matter less |
|
None |
Predict a bounded value in (0, 1) |
|
|
Linear regression — temperature prediction¶
import numpy as np
from snn.model import Sequential
from snn.layers import Dense
rng = np.random.default_rng(1)
# y = 3x + 7 + noise
X = rng.uniform(-5, 5, size=(500, 1))
y = 3 * X + 7 + rng.normal(0, 0.5, size=(500, 1))
model = Sequential([
Dense(1), # a single neuron with no activation is linear regression
])
model.compile("sgd", "mse", learning_rate=0.01)
model.fit(X, y, epochs=100, batch_size=32, verbose=0)
# should be very close to W≈3, b≈7
W = model._layers[0].W
b = model._layers[0].b
print(f"Learned: y = {W[0,0]:.3f}x + {b[0,0]:.3f}")
Polynomial regression — y = x²¶
A single linear layer cannot fit a curve. You need hidden layers with non-linear activations. Here we compare several activations on y = x²:
import numpy as np
from snn.model import Sequential
from snn.layers import Dense
from snn.utils import train_test_split
rng = np.random.default_rng(42)
X = np.linspace(-3, 3, 600).reshape(-1, 1)
y = X ** 2 # perfect quadratic, no noise
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
def build(activation):
m = Sequential([
Dense(64, activation=activation),
Dense(64, activation=activation),
Dense(32, activation=activation),
Dense(1),
])
m.compile("adam", "mse", metrics=["r2"], learning_rate=3e-3)
m.fit(X_train, y_train, epochs=500, batch_size=64, verbose=0)
return m.evaluate(X_test, y_test, verbose=0)
for act in ["relu", "gelu", "sine", "bent_identity", "squareplus"]:
res = build(act)
print(f" {act:<16} MSE={res['loss']:.5f} R²={res['r2']:.4f}")
Expected results:
relu MSE=0.00012 R²=1.0000
gelu MSE=0.00025 R²=1.0000
sine MSE=0.03142 R²=0.9952
bent_identity MSE=0.00186 R²=0.9997
squareplus MSE=0.00014 R²=1.0000
All activations achieve R² ≥ 0.995.
Huber loss — robust to outliers¶
MSE penalises large errors with x². If your dataset has outliers, the Huber loss is more robust:
model.compile(
"adam",
{"name": "huber", "delta": 1.5}, # dict config — set delta directly
metrics=["r2"],
)
delta controls the crossover point:
Errors smaller than
delta→ behaves like MSE (quadratic)Errors larger than
delta→ behaves like MAE (linear)
A large delta (e.g. 5.0) is close to MSE; a small delta (e.g. 0.1) is
close to MAE.
Multiple outputs¶
Predict several values at once by setting the output layer’s units > 1:
# Predict (sin(x), cos(x)) simultaneously
X = np.linspace(0, 2*np.pi, 500).reshape(-1, 1)
y = np.column_stack([np.sin(X), np.cos(X)]) # shape (500, 2)
model = Sequential([
Dense(64, activation="gelu"),
Dense(64, activation="gelu"),
Dense(2), # two outputs
])
model.compile("adam", "mse", metrics=["r2"])
model.fit(X, y, epochs=300, batch_size=32)
Gradient clipping — when training is unstable¶
If your loss explodes during training, gradient clipping limits how large any single update can be:
model.compile(
"adam", "mse",
clip_norm=1.0, # clip global gradient norm to 1.0
)
This is equivalent to Trainer(model, clip_grad_norm=1.0) but requires no
extra imports.
Evaluating regression¶
Metric |
String key |
Good value |
|---|---|---|
Mean Squared Error |
|
Near 0 |
Mean Absolute Error |
|
Near 0 |
R² (coefficient of determination) |
|
Near 1.0 |
model.compile("adam", "mse", metrics=["r2", "mae"])
What next?¶
Tutorial 4 — detailed guide to every activation, when to use each one, and how they affect learning.
Tutorial 5 — LSTM and GRU for time-series data.