import {ms, msStyle} from "/assets/js/ms-plot.js"
viewof x1 = Inputs.range([-2, 2], {value: 1.0, step: 0.1, label: "Input x₁"})
viewof x2 = Inputs.range([-2, 2], {value: 0.5, step: 0.1, label: "Input x₂"})
viewof x3 = Inputs.range([-2, 2], {value: -1.0, step: 0.1, label: "Input x₃"})
viewof w1 = Inputs.range([-3, 3], {value: 2.0, step: 0.1, label: "Weight w₁"})
viewof w2 = Inputs.range([-3, 3], {value: -1.0, step: 0.1, label: "Weight w₂"})
viewof w3 = Inputs.range([-3, 3], {value: 0.3, step: 0.1, label: "Weight w₃"})
viewof bias = Inputs.range([-3, 3], {value: 0.0, step: 0.1, label: "Bias b"})
viewof act = Inputs.radio(["none (linear)", "ReLU", "sigmoid"], {value: "ReLU", label: "Activation f"})From Linear Regression to Deep Learning: Your First Neural Network
ML
Deep Learning
What a weight really is, why a single neuron is just linear regression, and how hidden layers and depth let a network learn curves. Interactive, step by step.
Deep learning sounds mysterious, but its building block is something you already know: multiply inputs by weights, add them up. That is linear regression. A neural network simply stacks many of these small calculations in layers, with a bend (an activation) in between. Stack enough layers and you have deep learning.
TipWhat you will learn
- What a neuron and a weight are, with a live diagram
- Why one neuron with no bend is linear regression, weight by weight
- How a hidden layer lets the model learn a curve linear regression cannot
- How an ANN and a deeper DNN solve a five-feature problem
- How to save the trained model and load it again
The building block: one neuron
A neuron takes inputs x_1, x_2, \dots, multiplies each by its own weight, adds a bias, then passes the total through an activation function f:
z = w_1 x_1 + w_2 x_2 + w_3 x_3 + b, \qquad a = f(z) \tag{1}
ImportantWhat is a weight, exactly?
A weight is a single number attached to one connection. It answers one question: if this input goes up by 1, how much does it push the neuron’s total z, and in which direction?
- Large weight: the input matters a lot. Near zero: the input is almost ignored.
- Positive weight: the input pushes the total up. Negative: it pushes it down.
- The bias b is the starting value before any input is added.
Think of each weight as a volume knob on one microphone: it sets how loud, and with what sign, that input is heard. Training means turning all the knobs until the predictions match the data.
Play with the knobs. Line thickness shows the size of each weight, colour shows its sign.
ReLU (rectified linear unit) is the most common activation: it outputs \max(0, z), so it is zero until the total crosses zero, then rises in a straight line. That one bend is what gives neural networks their power.
Weights vs linear regression
Linear regression with one feature is \hat{y} = \beta_0 + \beta_1 x. Now look at a neuron with one input and no activation: \hat{y} = b + w\,x. Same formula, different names.
| Linear regression | Neural network | |
|---|---|---|
| Multiplier on an input | Coefficient \beta_1 (slope) | Weight w |
| Constant term | Intercept \beta_0 | Bias b |
| How it is found | One exact formula (least squares) | Gradual adjustment (gradient descent) |
| Meaning of the multiplier | Fixed effect of x everywhere | Effect of x on one neuron; the total effect depends on all neurons |
| Can it bend? | No, always a straight line | Yes, once a hidden layer with activation is added |
| Number of parameters | Features + 1 | Can be thousands or billions |
Experiment 1: one feature, one target
A building’s electricity demand depends on the outside temperature. Below about 22 °C demand is flat. Above it, the air conditioning turns on and demand climbs. We simulate 120 days.
Simulate 120 days of temperature and demand
import numpy as np, pandas as pd, warnings
warnings.filterwarnings("ignore")
from sklearn.linear_model import LinearRegression
from sklearn.neural_network import MLPRegressor
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import make_pipeline
from sklearn.compose import TransformedTargetRegressor
from sklearn.model_selection import train_test_split
from sklearn.metrics import r2_score, mean_absolute_error
import plotly.graph_objects as go
from ms_charts import COLORS, PLOTLY_CONFIG
rng = np.random.default_rng(7)
n = 120
temp = rng.uniform(5, 40, n) # °C
cool = np.maximum(temp - 22, 0) # air conditioning kicks in at 22 °C
demand = 50 + 2.6 * cool + 0.04 * cool**2 + rng.normal(0, 3, n) # kW
x = temp.reshape(-1, 1)
grid = np.linspace(5, 40, 200)Model A: linear regression vs a single neuron
We fit ordinary linear regression, then train one neuron with no activation using gradient descent: start with w = 0, b = 0, measure the error, nudge both numbers downhill, and repeat.
Train both models
lr = LinearRegression().fit(x, demand)
# a single neuron trained by gradient descent (on scaled x for stability, then converted back to °C)
mu, sd = temp.mean(), temp.std()
z = (temp - mu) / sd
w, b, eta, path = 0.0, 0.0, 0.1, []
for epoch in range(80):
pred = w * z + b
err = pred - demand
path.append(dict(epoch=epoch, w=w / sd, b=b - w * mu / sd, mse=float((err**2).mean())))
w -= eta * 2 * (err * z).mean() # gradient of mean squared error w.r.t. w
b -= eta * 2 * err.mean() # gradient w.r.t. b
w_neuron, b_neuron = w / sd, b - w * mu / sdDrag the slider to watch the neuron learn. Its line starts flat at zero and settles exactly on the linear regression line.
| Parameter | Linear regression | Single neuron | Same? |
|---|---|---|---|
| Slope / weight | 1.644 | 1.644 | Yes |
| Intercept / bias | 26.79 | 26.79 | Yes |
| R² | 0.758 | 0.758 | Yes |
Similarity: with no hidden layer and no activation, the neuron’s weight is the regression slope and its bias is the intercept. Both say “each extra degree adds about 1.64 kW, everywhere”. Difference: regression solves it in one exact step, the neuron gets there by trial and correction. Both are wrong in the same way: the data bends, a straight line cannot.
Experiment 2: five features, one target
Real buildings depend on more than temperature. We simulate 3,000 hourly readings with five features:
| Feature | Meaning |
|---|---|
temp |
Outside temperature (°C) |
humidity |
Relative humidity (%) |
hour |
Hour of day (0 to 23) |
weekend |
1 on Saturday and Sunday |
occupancy |
People in the building |
Simulate 3,000 hourly readings
N = 3000
F = pd.DataFrame({
"temp": rng.uniform(-5, 40, N),
"humidity": rng.uniform(20, 90, N),
"hour": rng.integers(0, 24, N),
"weekend": (rng.random(N) < 2 / 7).astype(int),
})
F["occupancy"] = rng.uniform(0, 200, N) * (1 - 0.7 * F.weekend)
def true_demand(t, h, hr, wk, occ):
open_ = ((hr >= 8) & (hr < 19)).astype(float) * (1 - wk) # office open on weekdays
return (60 + 0.10 * (t - 18)**2 # heating and cooling (U shape)
+ 0.04 * h * np.maximum(t - 24, 0) # humid heat is extra costly
+ 35 * open_ + 0.25 * occ * open_) # lights, computers, people
y = true_demand(F.temp, F.humidity, F.hour, F.weekend, F.occupancy) + rng.normal(0, 6, N)
Xtr, Xte, ytr, yte = train_test_split(F, y, test_size=0.25, random_state=0)Linear regression first, then a network with one hidden layer of 8 neurons. We scale the inputs and the target, which helps the network train reliably.
def network(layers, seed=0):
mlp = MLPRegressor(hidden_layer_sizes=layers, activation="relu",
solver="lbfgs", alpha=1e-3, max_iter=3000,
random_state=seed)
net = make_pipeline(StandardScaler(), mlp) # scale inputs
return TransformedTargetRegressor(regressor=net, # scale target
transformer=StandardScaler())
linreg = make_pipeline(StandardScaler(), LinearRegression()).fit(Xtr, ytr)
ann = network((8,)).fit(Xtr, ytr)This is the map of the network we just trained: 5 inputs, 8 hidden neurons, 1 output. Hover any line to read its weight.
Blue lines are positive weights, red are negative, thicker means stronger. With 5 × 8 + 8 × 1 = 48 weights plus 9 biases, the ANN has 57 parameters. Linear regression has 6.
Going deep: the DNN
A deep neural network (DNN) is simply a network with two or more hidden layers. The first layer learns simple patterns (for example “it is hot” or “the office is open”), the next layer combines them (“hot and open”). That stacking of simple ideas into richer ones is what the “deep” in deep learning means.
# two hidden layers: 8 neurons, then 6 neurons
dnn = network((8, 6)).fit(Xtr, ytr)How the three models compare
| Model | Parameters | Test R² | Mean error |
|---|---|---|---|
| Linear regression | 6 | 0.181 | 27.2 kW |
| ANN (1 hidden layer: 8) | 57 | 0.871 | 9.6 kW |
| DNN (2 hidden layers: 8, 6) | 109 | 0.969 | 5.2 kW |
Linear regression cannot capture the U-shaped temperature effect or the office switching on and off, so it explains very little. The ANN captures most of it, and the extra layer in the DNN lifts accuracy further. On a small problem like this, a wider single layer can also get close. Depth pays off most on complex data such as images, audio and text.
Try it yourself
Change the conditions and watch all three models respond. The “true” value comes from the simulation formula, without noise.
Set the hour to 2 (night) or switch on Weekend: the networks drop sharply, the linear model barely moves.
Save the model and load it later
Training takes time, so you save the trained model once and reuse it. With scikit-learn, joblib stores the whole pipeline: the scalers, every weight and every bias.
import joblib, os
path = "models/building_demand_dnn.joblib"
os.makedirs("models", exist_ok=True)
joblib.dump(dnn, path) # save
loaded = joblib.load(path) # load, any time later
new_hour = pd.DataFrame([{"temp": 33, "humidity": 70, "hour": 14,
"weekend": 0, "occupancy": 120}])
print(f"Saved file size: {os.path.getsize(path) / 1024:.0f} KB")
print(f"Predicted demand: {loaded.predict(new_hour)[0]:.1f} kW")
same = np.allclose(loaded.predict(Xte), dnn.predict(Xte))
print("Same predictions as the original:", same)Saved file size: 10 KB
Predicted demand: 174.2 kW
Same predictions as the original: True
The same idea in the two most popular deep learning libraries:
keras_save.py
model.save("building_demand_dnn.keras")
model = keras.models.load_model("building_demand_dnn.keras")torch_save.py
torch.save(model.state_dict(), "building_demand_dnn.pt") # weights only
model = DemandNet() # same architecture
model.load_state_dict(torch.load("building_demand_dnn.pt"))
model.eval()
WarningOnly load files you trust
joblib, pickle and older PyTorch files can run code when loaded. Never load a model file from an unknown source.
The journey in one picture
flowchart LR A["Linear regression<br/>y = b + w·x"] --> B["One neuron<br/>same thing"] --> C["Hidden layer + ReLU<br/>ANN: learns bends"] --> D["2+ hidden layers<br/>DNN: combines patterns"] --> E["Save with joblib<br/>reuse anywhere"]
Summary
- A weight is the number that says how much, and in which direction, one input pushes one neuron.
- A neuron with no activation and no hidden layer is linear regression: weight = slope, bias = intercept.
- A hidden layer with ReLU adds bends. No single weight is the slope any more; the effect of an input depends on which neurons are switched on.
- A DNN stacks two or more hidden layers to combine simple patterns into richer ones.
- Save the trained model so you can reuse it without retraining.
For the full theory, see Goodfellow et al. (2016).
References
Goodfellow, Ian, Yoshua Bengio, and Aaron Courville. 2016. Deep Learning. MIT Press. https://www.deeplearningbook.org.