Deep Learning Linear Regression: Why It Comes First

Train a one-parameter linear regression model in C++ and Python and watch the predict, measure, update loop of deep learning happen with real numbers.

You decide to learn deep learning, open the first chapter, and find a straight line instead of a neural network. It is easy to feel let down, or to wonder why you are back at y = ax + b. But when you implement deep learning linear regression yourself in C++, you find that this simple line already contains almost the entire skeleton of how deep learning trains.

Series guide: See all 11 parts and what is already published in the Deep Learning Math in C++ roadmap.

The short answer

Deep learning starts with linear regression because its training loop, predict → measure the error → compute the gradient → update the parameter, is the same loop a huge neural network uses. With only one parameter you can follow every step with your own eyes, and because you know the right answer in advance, you can check whether training actually worked.

What this post answers

  • What “training” means as an actual computation
  • Three reasons linear regression is the right starting point
  • How θ really changes when trained in C++
  • Why training ends at 2.97 instead of the true value 3

Key deep learning terms first

These terms appear throughout this post and the whole series. The right column shows what each term refers to in this post’s experiment.

TermMeaningIn this experiment
ModelA formula that turns an input into a predictionŷ = θx
ParameterA value inside the model that training changes. “Training” means changing parametersthe single θ
WeightA parameter multiplied by an input; how strongly the input affects the resultθ (the slope)
BiasA parameter added regardless of the input; the intercept of a linenone (added in part 7 as ŷ = θ1·x + θ0)
SampleOne input–answer pair (x, y)100 samples
PredictionThe model’s answer, written ŷ (“y hat”)θ × x
LossHow wrong the prediction is for one sample(y − ŷ)²
CostThe average loss over several samplescovered in part 6
GradientHow much, and in which direction, the loss changes when a parameter changes; found by differentiation−2x(y − ŷ)
Gradient descentReducing the loss by moving parameters a little against the gradientthe θ update rule
Learning rateHow large one update step is; written α or lr0.001
Update (step)Changing the parameters once; also called an iteration5,000 in total
EpochOne full pass over all the data50 epochs (1 epoch = 100 updates)
BatchThe samples used together for one updateone sample at a time (compared in part 6)

Two notes:

  • Weight and bias: in neural networks, people often call all weights and biases together “parameters” or just “weights”. The θ here multiplies the input x, so it is a weight. The word bias is shared with the statistical term for systematic skew, but inside a model it simply means the constant term that is added.
  • Epochs and updates: with 100 samples and one update per sample, one epoch is 100 updates, so 50 epochs are 5,000 updates. The same training looks different depending on whether a chart’s x-axis is updates or epochs, so always check the unit.

A deep learning linear regression experiment: the answer is 3

First we build a problem whose answer we know. We draw 100 random inputs x and make the targets y by adding a little noise to 3x.

  • Data: y = 3x + 0.5 × (standard normal noise), 100 samples
  • Model: ŷ = θx (a single slope θ, no intercept)
  • Start: θ = 0.1 (deliberately far from 3)

The model does not know the answer is 3. The question is whether, from the data alone, it can move θ close to 3 by itself.

Deep learning linear regression result: the starting line (θ=0.1) and the trained line (θ=2.971) drawn over the scattered data points

The starting line (dashed, θ=0.1) is almost flat and explains nothing. The trained line (red, θ=2.971) runs right through the middle of the data. This post is about what happens while the dashed line turns into the red one.

Training is these four steps, repeated

Each time a sample (x, y) comes in, the model does four things.

  1. Predict: compute ŷ = θx with the current θ.
  2. Measure the error: loss = (y − ŷ)². Squaring makes large mistakes count much more.
  3. Compute the gradient: use the derivative to see whether making θ a little bigger increases or decreases the loss.
  4. Update: move θ a little in the direction that reduces the loss.

Put together, the rule for changing θ becomes a single line.

θ ← θ + 2 · α · x · (y − ŷ)

α (the learning rate) sets how big each move is. Part 2 (deriving the gradient descent update rule) shows where this line comes from. For now it is enough to read it as “correct θ in proportion to the error (y − ŷ) and the input size (x)”.

The same loop in C++ and Python

Here are the four steps as code, side by side. Each program has a data part and a training part, and the core of the training part is three lines in both languages.

C++ (C++17, no external libraries)

#include <iostream>
#include <random>
#include <vector>

int main() {
    // Make the data: y = 3x + 0.5 * noise
    std::mt19937 rng(0);                                   // fixed random seed
    std::normal_distribution<double> normal(0.0, 1.0);
    const std::size_t n_sample = 100;
    std::vector<double> x_data(n_sample), y_data(n_sample);
    for (auto& x : x_data) x = normal(rng);                // x ~ N(0, 1)
    for (std::size_t i = 0; i < n_sample; ++i)
        y_data[i] = 3.0 * x_data[i] + 0.5 * normal(rng);   // y = 3x + noise

    // Train
    double th = 0.1;                                       // start far from 3
    const double lr = 0.001;                               // learning rate
    for (std::size_t epoch = 0; epoch < 50; ++epoch) {     // 50 passes over the data
        for (std::size_t i = 0; i < n_sample; ++i) {
            const double x = x_data[i], y = y_data[i];
            const double pred = th * x;                    // 1) predict
            [[maybe_unused]] const double loss = (y - pred) * (y - pred);  // 2) error
            th = th + 2.0 * x * lr * (y - pred);           // 3)+4) gradient and update
        }
    }
    std::cout << "learned th = " << th << 'n';
}

Python (NumPy)

import numpy as np

# Make the data: y = 3x + 0.5 * noise
np.random.seed(0)                                       # fixed random seed
n_sample = 100
x_data = np.random.randn(n_sample)                      # x ~ N(0, 1)
y_data = 3 * x_data + 0.5 * np.random.randn(n_sample)   # y = 3x + noise

# Train
th = 0.1                                                # start far from 3
lr = 0.001                                              # learning rate
for epoch in range(50):                                 # 50 passes over the data
    for x, y in zip(x_data, y_data):
        pred = th * x                                   # 1) predict
        loss = (y - pred) ** 2                          # 2) error
        th = th + 2 * x * lr * (y - pred)               # 3)+4) gradient and update

print("learned th =", th)

Side by side, the training parts match line for line; only the syntax differs. There are no library calls, just multiplication, subtraction, and addition. A huge neural network has the same loop. The differences are that it has billions of parameters instead of one, and it computes gradients with backpropagation instead of a hand-derived formula. In the C++ code, [[maybe_unused]] marks that loss is computed only to show the step, which stops the compiler’s “unused variable” warning.

The real difference is in making the data. Python fills an array with one call to np.random.randn, while C++ needs a random engine (std::mt19937) and a distribution (std::normal_distribution) and fills the array in a loop. The two languages also use different random algorithms, so the same seed 0 produces different data.

RunDataLearned θ
C++C++ random numbers (mt19937, seed 0)2.97091
PythonNumPy random numbers (seed 0)3.06161
PythonReads the data C++ generated2.97091

Python’s 3.06161 is not wrong; it is the result of training on 100 different samples. The least-squares answer for that data is 3.05955, and training went right next to it. When Python is given the data C++ generated, it learns exactly 2.97091. Different languages, same training computation.

The results and charts below use the C++ run.

What the run shows

100 samples, 50 epochs, 5,000 updates in total:

ItemValue
Starting θ0.1
Learned θ2.97091
True value3
First update with |θ − 3| < 0.1update 2,100 (epoch 22)
Mean loss in the last epoch0.2622

θ rising from 0.1 toward 3 as the number of updates grows

θ climbs fast at first and slows down as it approaches 3. Far from the answer, the error (y − ŷ) is large, so each correction is large; near the answer, the error is small, so corrections shrink. Nobody told the model to slow down. The update rule itself behaves that way.

Reason 1. It shows the training loop of a neural network at its smallest

The predict → error → gradient → update loop in the code above is the same structure most deep learning models use. Image classifiers and language models also repeat “predict with the current parameters, measure how wrong that was, move the parameters slightly so it is less wrong” a very large number of times.

Linear regression keeps that loop and shrinks only the model part to its simplest form. Once the loop is clear here, a more complex model reads as “the model changed, the training did not”.

Reason 2. The error landscape is a parabola you can see

For a single sample, the loss is a quadratic function of θ: a parabola. It has one bottom, so there is never any doubt about which way to go.

Loss parabolas over θ for x=1 and x=2, with dots showing θ descending to the bottom at θ=3

The two parabolas are the loss of one sample on y = 3x, with x=1 and x=2. Both touch zero loss at θ = 3. The red dots (x=1) and orange dots (x=2) mark θ every 10 updates with learning rate 0.01. The steeper x=2 side reaches the bottom much faster: getting within 0.01 of 3 took 281 updates for x=1 but only 68 for x=2.

Here are the first four updates by hand (x = 1, y = 3, learning rate 0.01):

UpdateθPrediction ŷLossGradientNext θ
00.10000.10008.4100−5.80000.1580
10.15800.15808.0770−5.68400.2148
20.21480.21487.7571−5.57030.2705
30.27050.27057.4499−5.45890.3251

A negative gradient means “increasing θ lowers the loss”, so θ grows. And the remaining distance 3 − θ shrinks by exactly 0.98 each time. That 0.98 decides both the speed of training and whether it blows up, and it comes back in part 3 (learning rate).

You cannot draw this landscape for a model with billions of parameters. With linear regression you can, so “walking down the slope” stops being a metaphor and becomes a picture.

Reason 3. You know the answer, so you can check it

In real problems you never know the right parameters. When training goes badly, it is hard to tell whether the code is wrong or the data is hard. This experiment was built with the answer 3, so the result can be checked directly.

Yet the learned θ was 2.97091, not 3. Did training fail?

The slope that fits this exact data best (the least-squares solution, Σxy / Σx²) is 2.96927. With noise mixed into only 100 samples, “the slope that fits this data best” is a little below 3, and training went right to it. The gap from 3 comes from the noise in the data, not from a training mistake.

Having a known answer to compare against is what lets you say “training worked” with numbers. That is one more reason linear regression is a good first step.

Three results that are easy to misread

A loss that never reaches zero is not a failure

The mean loss in the last epoch was 0.2622. The noise added to the data has variance 0.5² = 0.25, so no amount of training can push the loss much below that. “The loss stopped going down” does not mean “training failed”.

θ keeps wobbling after training

Because this experiment updates θ after every sample, θ kept moving between 2.9627 and 2.9730 during the last epoch. Each sample pulls in a slightly different direction. Reducing that wobble is the topic of part 6 (batch, SGD, and mini-batch).

A bigger learning rate is not always faster

The learning rate 0.001 is safe but slow: reaching within 0.1 of 3 took 22 epochs. A larger learning rate is faster, but past a certain point θ overshoots the bottom and moves away. Part 3 finds that boundary with real numbers.

Deep learning linear regression in summary

  • Deep learning training is a loop of predict → error → gradient → update, and linear regression shows that loop at its smallest.
  • With one parameter and a parabola-shaped error landscape, you can follow training to the end with charts and numbers.
  • Starting from a known answer lets you verify the result, including the fact that 2.97 is “the value that fits this data best”.

The next part derives the update rule θ ← θ + 2αx(y − ŷ), which we took on trust here, with a single derivative.

Try it yourself

If you can run the code, change just two things.

  1. Start from θ = 6.0 instead of θ = 0.1. Check whether θ now comes down from above and lands near the same value.
  2. Set the noise size from 0.5 to 0. The least-squares answer becomes exactly 3; check whether the learned θ gets very close to 3.

If both behave as expected, you have seen for yourself that training is not “knowing the answer” but “walking down to the bottom the data points to”.