Inventing a Loss Function

You have a model that predicts house prices. Some predictions are too high, some too low. You need one number that tells you how wrong the model is overall — so you can tweak each weight and watch the error change. By the end of this chapter you will have invented that number from scratch.

Part 1: The Prediction Error Problem~15 min

Imagine you're building a model that predicts house prices (in lakhs) for 5 different houses at a time. You want to compute the overall error of this model — a single number — so that you can try tweaking each individual weight and see how that weight impacts the overall error.

Here are the actual prices and your model's predictions:

House Actual Predicted
1 1.0 0.9
2 1.1 2.0
3 0.9 0.7
4 0.5 0.3
5 3.0 2.0

Your job: come up with a formula that gives one number representing how wrong the predictions are overall.

Exercise 1.1 — Can We Just Sum the Differences?

The simplest idea: for each house, compute predicted - actual, then add them all up.

Try it. What number do you get? Does that number accurately reflect how wrong the predictions are?

Provided — data
actual    = [1.0, 1.1, 0.9, 0.5, 3.0]
predicted = [0.9, 2.0, 0.7, 0.3, 2.0]
Hint 1

Compute sum(p - a for a, p in zip(actual, predicted)). What do you get?

Hint 2

The result is close to zero — but every single prediction is wrong! The negative errors (under-predictions) cancel the positive errors (over-predictions). That's the problem.

Solution

The sum is -0.5, which looks tiny. But look at house 2: the prediction is off by 0.9, and house 5 is off by 1.0. The positive and negative errors cancel each other out. We need a formula where errors can't cancel.

actual    = [1.0, 1.1, 0.9, 0.5, 3.0]
predicted = [0.9, 2.0, 0.7, 0.3, 2.0]

total_error = sum(p - a for a, p in zip(actual, predicted))
print(f"Sum of differences: {total_error}")
# Output: -0.5
# This is misleading — every prediction is wrong, but the sum is near zero.

Exercise 1.2 — Fix the Cancellation

The problem is clear: positive and negative differences cancel each other out.

Think of at least two different mathematical operations that would prevent this cancellation — operations that make every error contribute positively to the total, regardless of whether the prediction was too high or too low.

Your answer:

Idea 1:

Your answer:

Idea 2:

Hint 1

What operations turn a negative number into a positive one?

Hint 2

There are two classic choices: one that removes the sign, and one that makes everything positive by multiplying.

Solution

Idea 1 — Absolute value: Take |predicted - actual| for each house. Negatives become positive, so nothing cancels.

Idea 2 — Squaring: Take (predicted - actual)² for each house. Any number squared is positive, so nothing cancels.

These are exactly the two approaches the ML community uses!

What you just invented:

You independently arrived at the two most important loss functions in machine learning:

The entire field of regression uses one of these (or a combination). Let's formalise them.

Part 2: Two Loss Functions~15 min

Let's give proper names and formulas to your two ideas.

Mean Squared Error (MSE):

$$\text{MSE} = \frac{1}{n}\sum_{i=1}^{n}(y_i - \hat{y}_i)^2$$

Mean Absolute Error (MAE):

$$\text{MAE} = \frac{1}{n}\sum_{i=1}^{n}|y_i - \hat{y}_i|$$

Here $y_i$ is the actual value and $\hat{y}_i$ is the predicted value.

Below are implementations of both. Study them — you'll use them to compare behavior.

Exercise 2.1 — Compare on the House Data

Compute MSE and MAE on the original house data. Then imagine house 5's prediction goes wildly wrong — say predicted[4] = 100 instead of 2.0. Compute both metrics again.

  1. Which metric changes more dramatically with the outlier?
  2. Why does that happen?
Provided — loss functions
def compute_mse(actual, predicted):
    n = len(actual)
    return sum((a - p) ** 2 for a, p in zip(actual, predicted)) / n

def compute_mae(actual, predicted):
    n = len(actual)
    return sum(abs(a - p) for a, p in zip(actual, predicted)) / n
Provided — data
actual    = [1.0, 1.1, 0.9, 0.5, 3.0]
predicted = [0.9, 2.0, 0.7, 0.3, 2.0]

# Normal case
print(f"MSE: {compute_mse(actual, predicted):.4f}")
print(f"MAE: {compute_mae(actual, predicted):.4f}")
Hint 1

Create a second predicted list with predicted_outlier = [0.9, 2.0, 0.7, 0.3, 100.0] and compute both metrics on it.

Hint 2

Squaring a large number makes it enormous. (3 - 100)² = 9409, while |3 - 100| = 97. That's why MSE explodes.

Solution

MSE jumps from 0.49 to 1882 — a 3800× increase. MAE goes from 0.56 to 20 — about 36×. MSE is far more sensitive to outliers because squaring amplifies large errors.

actual    = [1.0, 1.1, 0.9, 0.5, 3.0]
predicted = [0.9, 2.0, 0.7, 0.3, 2.0]

print("Normal data:")
print(f"  MSE: {compute_mse(actual, predicted):.4f}")   # 0.488
print(f"  MAE: {compute_mae(actual, predicted):.4f}")   # 0.560

predicted_outlier = [0.9, 2.0, 0.7, 0.3, 100.0]
print("\nWith outlier (house 5 predicted=100):")
print(f"  MSE: {compute_mse(actual, predicted_outlier):.4f}")   # 1882.088
print(f"  MAE: {compute_mae(actual, predicted_outlier):.4f}")   # 19.960

Exercise 2.2 — Drawbacks of Each

You've just seen one drawback of MSE. Let's think more carefully:

  1. MSE problem: When a single prediction is very wrong (an outlier), what happens to the MSE? Is this good or bad for training a model?

  2. MAE — any issues? Think about the shape of |x| as a function. Is there anything unusual about it at x = 0? (We'll investigate this in the next part.)

Your answer:

MSE drawback:

Your answer:

Possible MAE issue:

Hint 1

For MSE: squaring makes large errors dominate. A single outlier can make the model ignore all other data points to fix that one mistake.

Hint 2

For MAE: plot y = |x|. What does the graph look like at x = 0? Is it smooth or does it have a sharp corner?

Solution

MSE: Squaring amplifies large errors disproportionately. One outlier can dominate the entire loss, pulling the model toward that single point.

MAE: The function |x| has a sharp corner (a "kink") at x = 0. This will turn out to be a problem when we try to compute gradients — which is essential for training ML models.

Part 3: Why Gradients Matter~20 min

Remember why we wanted a loss function in the first place? We said:

"We need to tweak each individual weight and see how that weight impacts the overall error."

That's exactly what differentiation does. The gradient of the loss with respect to a parameter tells you: if I nudge this parameter a tiny bit, how much does the loss change?

Our training algorithm (gradient descent) depends on this. So our loss function must be differentiable — it must have a well-defined gradient everywhere.

Let's check whether our two loss functions qualify.

To keep things simple, let's focus on a single prediction. Define:

$$\text{diff} = y - \hat{y}$$

Then our two per-element losses are:

We want to compute $\frac{dL}{d(\text{diff})}$ for each — how the loss changes as the prediction error changes.

Exercise 3.1 — Gradient of Squared Error

For the squared loss $L = \text{diff}^2$, compute the gradient $\frac{dL}{d(\text{diff})}$ using the power rule.

Then verify your formula numerically: pick a few values of diff (e.g. -2, 0, 1, 3), compute the gradient using finite differences $\frac{L(\text{diff} + \epsilon) - L(\text{diff} - \epsilon)}{2\epsilon}$ with a small $\epsilon$, and check that it matches your analytical formula.

Provided — verification helper
def numerical_gradient(loss_fn, diff, eps=1e-6):
    return (loss_fn(diff + eps) - loss_fn(diff - eps)) / (2 * eps)

def squared_loss(diff):
    return diff ** 2
Hint 1

Power rule: $\frac{d}{dx}x^2 = 2x$. So the gradient of $\text{diff}^2$ is $2 \cdot \text{diff}$.

Hint 2

Verify:

for d in [-2, 0, 1, 3]:
    analytical = 2 * d
    numerical  = numerical_gradient(squared_loss, d)
    print(f"diff={d:+}: analytical={analytical:+.4f}, numerical={numerical:+.6f}")
Solution

The gradient of $\text{diff}^2$ is $2 \cdot \text{diff}$. Notice that when the error is large (e.g. diff=3), the gradient is also large (6). The gradient grows without bound as the error grows — this is why MSE reacts so strongly to outliers.

def squared_gradient(diff):
    return 2 * diff

for d in [-2, -1, 0, 1, 3]:
    analytical = squared_gradient(d)
    numerical  = numerical_gradient(squared_loss, d)
    print(f"diff={d:+}: analytical={analytical:+.4f}, numerical={numerical:+.6f}")

Exercise 3.2 — Gradient of Absolute Error

Now for the absolute loss $L = |\text{diff}|$, compute the gradient $\frac{dL}{d(\text{diff})}$.

Think piecewise:

Then verify numerically for diff values: -2, -0.5, 0.5, 3.

Provided — setup
def abs_loss(diff):
    return abs(diff)
Hint 1

When diff > 0: |diff| = diff, so the derivative is +1. When diff < 0: |diff| = -diff, so the derivative is -1.

Hint 2
for d in [-2, -0.5, 0.5, 3]:
    numerical = numerical_gradient(abs_loss, d)
    print(f"diff={d:+}: numerical gradient = {numerical:+.6f}")

You should see +1 for positive diff, -1 for negative diff.

Solution

The gradient is +1 for positive diff, -1 for negative diff. Unlike the squared loss, the gradient doesn't grow with the error — it's always ±1 regardless of how big the error is. That's good for outliers, but there's a problem...

def abs_gradient(diff):
    if diff > 0:
        return 1
    elif diff < 0:
        return -1
    else:
        return None  # undefined!

for d in [-2, -0.5, 0.5, 3]:
    analytical = abs_gradient(d)
    numerical  = numerical_gradient(abs_loss, d)
    print(f"diff={d:+.1f}: analytical={analytical:+}, numerical={numerical:+.6f}")

Exercise 3.3 — The Problem at Zero

You found that the gradient of |diff| is +1 when diff > 0, and -1 when diff < 0.

But what is the gradient when diff = 0 exactly? Is it -1 or +1?

Try computing the numerical gradient at diff = 0. Then think about what the graph of y = |x| looks like at x = 0. Is it smooth, or is there something unusual?

Your answer:

Gradient of |diff| at diff = 0:

Your answer:

What does the graph look like at that point?

Hint 1

At diff = 0, the function switches from -diff to +diff. The left side has slope -1, the right side has slope +1. There's no single tangent line — the function has a sharp corner.

Solution

The gradient is undefined at diff = 0. The function $|x|$ has a sharp corner (a "kink") at $x = 0$: approaching from the left the slope is -1, approaching from the right the slope is +1. There is no single well-defined tangent line.

In mathematical terms: $|x|$ is not differentiable at $x = 0$.

Why this matters:

Our training algorithm (gradient descent) works by computing the gradient of the loss and using it to update weights. If the gradient doesn't exist at some point, the algorithm breaks down.

So MSE has a problem with large errors, and MAE has a problem at zero error. Can we do better?

Part 4: The Best of Both Worlds~25 min

Here's our situation:

Small errors Large errors
Squared ($\text{diff}^2$) Smooth, differentiable Gradient explodes
Absolute ($ \text{diff} $) Not differentiable at 0 Gradient stays bounded (±1)

What if we could use the squared loss for small errors (where it's well-behaved) and the absolute loss for large errors (where it stays bounded)?

Exercise 4.1 — The Naive Combination

Here's the simplest idea. Pick some threshold $d$ and define:

$$L(\text{diff}) = \begin{cases} \text{diff}^2 & \text{if } |\text{diff}| \le d \ |\text{diff}| & \text{if } |\text{diff}| > d \end{cases}$$

Try $d = 1$. Now compute:

  1. The loss from the squared formula at $|\text{diff}| = d = 1$: what is $d^2$?
  2. The loss from the absolute formula at $|\text{diff}| = d = 1$: what is $|d|$?
  3. Are they equal?

Now compute the gradients at that same point:

  1. Gradient of $\text{diff}^2$ at diff = 1?
  2. Gradient of $|\text{diff}|$ at diff = 1?
  3. Are they equal?

What's the problem?

Your answer:

Value from squared formula at |diff|=1:

Your answer:

Value from absolute formula at |diff|=1:

Your answer:

Gradient from squared at diff=1:

Your answer:

Gradient from absolute at diff=1:

Your answer:

The problem:

Hint 1

Squared: $d^2 = 1^2 = 1$. Absolute: $|d| = 1$. They happen to be equal when $d = 1$, but the gradients are different: squared gives $2 \times 1 = 2$, absolute gives $1$.

Hint 2

Try $d = 2$: squared gives $4$, absolute gives $2$. Now even the values don't match! At the boundary, the function jumps — it's discontinuous. And the gradients never match (except by coincidence).

Solution

At $|\text{diff}| = d$:

  • Values: squared gives $d^2$, absolute gives $d$. These are only equal when $d = 1$ (a coincidence). For any other $d$, there's a jump in the function.
  • Gradients: squared gives $2d$, absolute gives $1$. These are never equal (unless $d = 0.5$, another coincidence).

The function has discontinuities in both value and gradient at the boundary. Gradient descent can't work with jumps — we need smoothness.

Exercise 4.2 — Visualize the Jump

Plot the naive combined loss for $\text{diff}$ ranging from -3 to 3, using $d = 1$.

You should see a clear discontinuity (jump) at diff = 1 and diff = -1 where the two formulas switch.

Also plot the individual squared and absolute losses on the same graph for comparison.

Provided — plotting setup
import numpy as np
import matplotlib.pyplot as plt

diff = np.linspace(-3, 3, 1000)
d = 1  # threshold

squared = diff ** 2
absolute = np.abs(diff)

# Naive combination: squared when |diff| <= d, absolute otherwise
naive = np.where(np.abs(diff) <= d, diff ** 2, np.abs(diff))
Hint 1
plt.figure(figsize=(8, 5))
plt.plot(diff, squared, '--', alpha=0.4, label='Squared (diff²)')
plt.plot(diff, absolute, '--', alpha=0.4, label='Absolute (|diff|)')
plt.plot(diff, naive, 'k-', linewidth=2, label='Naive combination')
plt.axvline(d, color='red', linestyle=':', alpha=0.5)
plt.axvline(-d, color='red', linestyle=':', alpha=0.5)
plt.legend()
plt.xlabel('diff')
plt.ylabel('Loss')
plt.title('Naive combination — spot the jump at ±d')
plt.show()
Solution

With $d = 1$ the values happen to match (both give 1), but the slopes are visibly different — the squared curve arrives with a steep slope while the absolute line departs with slope 1. Try $d = 2$ to see a clear value jump too.

plt.figure(figsize=(8, 5))
plt.plot(diff, squared, '--', alpha=0.4, label='Squared (diff²)')
plt.plot(diff, absolute, '--', alpha=0.4, label='Absolute (|diff|)')
plt.plot(diff, naive, 'k-', linewidth=2, label='Naive combination')
plt.axvline(d, color='red', linestyle=':', alpha=0.5, label=f'±d = ±{d}')
plt.axvline(-d, color='red', linestyle=':', alpha=0.5)
plt.legend()
plt.xlabel('diff')
plt.ylabel('Loss')
plt.title('Naive combination — spot the jump at ±d')
plt.grid(True, alpha=0.3)
plt.show()

The problem is clear: at the switchover point $|\text{diff}| = d$, the squared and absolute formulas disagree in both value and gradient.

We can't change the squared part — it's what gives us smoothness near zero, which is the whole reason we want it. So let's keep $\text{diff}^2$ for $|\text{diff}| \le d$ and modify the absolute part to connect seamlessly.

Instead of plain $|\text{diff}|$, we'll use:

$$L_{\text{outer}}(\text{diff}) = A \cdot |\text{diff}| + B$$

We need to find values of $A$ and $B$ such that at the boundary $|\text{diff}| = d$, the two pieces agree in both value and gradient.

Exercise 4.3 — Make It Smooth

We keep $\text{diff}^2$ for $|\text{diff}| \le d$. For $|\text{diff}| > d$, we use $A \cdot |\text{diff}| + B$.

At the boundary $|\text{diff}| = d$, we need two conditions:

  1. Values match: $d^2 = A \cdot d + B$
  2. Gradients match: the gradient of $\text{diff}^2$ at $\text{diff} = d$ must equal the gradient of $A \cdot |\text{diff}| + B$ at $\text{diff} = d$.

This gives you two equations and two unknowns ($A$ and $B$). Solve for $A$ and $B$ in terms of $d$.

Then write a Python function smooth_combined_loss(diff, d) using your values of $A$ and $B$, and plot it. Verify there's no jump or kink at $\pm d$.

Hint 1

The gradient of $\text{diff}^2$ is $2 \cdot \text{diff}$. At diff = $d$, that's $2d$. The gradient of $A \cdot |\text{diff}| + B$ (for diff > 0) is just $A$. So the gradient condition gives: $A = 2d$.

Hint 2

Substitute $A = 2d$ into the value equation: $d^2 = 2d \cdot d + B$, so $B = d^2 - 2d^2 = -d^2$.

Hint 3

Your combined loss should be:

$$L = \begin{cases} \text{diff}^2 & |\text{diff}| \le d \ 2d \cdot |\text{diff}| - d^2 & |\text{diff}| > d \end{cases}$$

def smooth_combined_loss(diff, d):
    if abs(diff) <= d:
        return diff ** 2
    else:
        return 2 * d * abs(diff) - d ** 2
Solution

$A = 2d$, $B = -d^2$. The curve is now perfectly smooth at the transition point — no jump in value, no jump in gradient.

# From gradient matching: A = 2d
# From value matching:    d² = 2d·d + B  →  B = -d²

def smooth_combined_loss(diff, d):
    if abs(diff) <= d:
        return diff ** 2
    else:
        return 2 * d * abs(diff) - d ** 2

# Verify at boundary: both sides should give the same value and gradient
d = 1
print(f"Inner side at diff=d:  value = {d**2},  gradient = {2*d}")
print(f"Outer side at diff=d:  value = {2*d*d - d**2},  gradient = {2*d}")

# Plot
diff_range = np.linspace(-3, 3, 1000)
loss_values = [smooth_combined_loss(x, d) for x in diff_range]

plt.figure(figsize=(8, 5))
plt.plot(diff_range, loss_values, 'k-', linewidth=2, label='Smooth combined loss')
plt.plot(diff_range, diff_range ** 2, '--', alpha=0.3, label='diff²')
plt.plot(diff_range, 2*d*np.abs(diff_range) - d**2, '--', alpha=0.3, label=f'2d|diff| - d²')
plt.axvline(d, color='red', linestyle=':', alpha=0.5)
plt.axvline(-d, color='red', linestyle=':', alpha=0.5)
plt.legend()
plt.xlabel('diff')
plt.ylabel('Loss')
plt.title('Smooth combined loss — no jump, no kink!')
plt.grid(True, alpha=0.3)
plt.show()

You just invented the Huber loss!

The standard formulation divides everything by 2 for convenience (so the squared part's gradient is $\text{diff}$ instead of $2 \cdot \text{diff}$):

$$L_{\delta}(\text{diff}) = \begin{cases} \frac{1}{2}\text{diff}^2 & |\text{diff}| \le \delta \ \delta \cdot |\text{diff}| - \frac{1}{2}\delta^2 & |\text{diff}| > \delta \end{cases}$$

This is mathematically identical to yours — just scaled by ½. It was published by the statistician Peter Huber in 1964 and is built into every modern ML framework: torch.nn.HuberLoss, tf.keras.losses.Huber, sklearn's HuberRegressor.

You derived it from scratch by asking three questions:

  1. How do we measure error without cancellation? → squared / absolute
  2. What are the problems with each? → outlier sensitivity / non-differentiability
  3. Can we combine them smoothly? → match value and gradient at the boundaryHuber loss

Part 5: Putting It All Together~10 min

Let's implement the standard (÷2) Huber loss and see how it compares to MSE and MAE on real data.

Exercise 5.1 — Implement Huber Loss

Write a function huber_loss(y_true, y_pred, delta) that returns the average Huber loss over all pairs. Use the standard formulation (with the ½ factor):

Test it:

huber_loss([5, 2, 7], [4.8, 2.5, 10], delta=1)  # ≈ 0.8817
huber_loss([1, 2, 3], [1, 2, 3], delta=1)        # 0.0
Hint 1

Loop over pairs, compute err = abs(y - yp), then apply the appropriate formula:

if err <= delta:
    total += 0.5 * (y - yp) ** 2
else:
    total += delta * err - 0.5 * delta ** 2
Solution
def huber_loss(y_true, y_pred, delta):
    total = 0
    for y, yp in zip(y_true, y_pred):
        err = abs(y - yp)
        if err <= delta:
            total += 0.5 * (y - yp) ** 2
        else:
            total += delta * err - 0.5 * delta ** 2
    return total / len(y_true)

print(huber_loss([5, 2, 7], [4.8, 2.5, 10], delta=1))  # ≈ 0.8817
print(huber_loss([1, 2, 3], [1, 2, 3], delta=1))        # 0.0

Exercise 5.2 — The Final Comparison

Plot all three loss functions — MSE, MAE, and Huber — for a single prediction as diff varies from -5 to 5. Use delta = 1.

Verify visually:

  1. Huber is smooth everywhere (no sharp corner at 0)
  2. Huber is quadratic near zero (matches MSE)
  3. Huber is linear in the tails (matches MAE)
  4. The transition at $\pm \delta$ is seamless
Provided — setup
import numpy as np
import matplotlib.pyplot as plt

diff = np.linspace(-5, 5, 1000)
delta = 1

mse = 0.5 * diff ** 2
mae = np.abs(diff)
Hint 1
huber = np.where(
    np.abs(diff) <= delta,
    0.5 * diff ** 2,
    delta * np.abs(diff) - 0.5 * delta ** 2
)

plt.figure(figsize=(8, 5))
plt.plot(diff, mse, '--', label='MSE (½ diff²)')
plt.plot(diff, mae, '--', label='MAE (|diff|)')
plt.plot(diff, huber, 'k-', linewidth=2, label=f'Huber (δ={delta})')
plt.legend()
plt.xlabel('diff')
plt.ylabel('Loss')
plt.title('MSE vs MAE vs Huber Loss')
plt.grid(True, alpha=0.3)
plt.show()
Solution

The Huber curve hugs the MSE parabola near zero, then peels off and tracks the MAE line for large errors — smooth everywhere, no kinks, no explosions.

huber = np.where(
    np.abs(diff) <= delta,
    0.5 * diff ** 2,
    delta * np.abs(diff) - 0.5 * delta ** 2
)

plt.figure(figsize=(8, 5))
plt.plot(diff, mse, '--', alpha=0.6, label='½ diff² (MSE)')
plt.plot(diff, mae, '--', alpha=0.6, label='|diff| (MAE)')
plt.plot(diff, huber, 'k-', linewidth=2, label=f'Huber (δ={delta})')
plt.axvline(delta, color='red', linestyle=':', alpha=0.4)
plt.axvline(-delta, color='red', linestyle=':', alpha=0.4)
plt.legend(fontsize=11)
plt.xlabel('diff', fontsize=12)
plt.ylabel('Loss', fontsize=12)
plt.title('MSE vs MAE vs Huber Loss', fontsize=13)
plt.grid(True, alpha=0.3)
plt.ylim(-0.5, 6)
plt.show()

Exercise 5.3 — Back to the Houses

Compute all three losses on the house price data — with and without the outlier.

actual    = [1.0, 1.1, 0.9, 0.5, 3.0]
normal    = [0.9, 2.0, 0.7, 0.3, 2.0]
outlier   = [0.9, 2.0, 0.7, 0.3, 100.0]

Use delta = 1. Which loss function gives the most sensible numbers in both scenarios?

Hint 1
for name, pred in [("Normal", normal), ("Outlier", outlier)]:
    print(f"{name}:")
    print(f"  MSE:   {compute_mse(actual, pred):.4f}")
    print(f"  MAE:   {compute_mae(actual, pred):.4f}")
    print(f"  Huber: {huber_loss(actual, pred, delta=1):.4f}")
Solution

With the outlier, MSE explodes to ~1882 while Huber stays at ~19.8 — similar to MAE's 20.0 but with the advantage of being fully differentiable. On normal data, Huber behaves like MSE, giving smooth gradients near zero. Best of both worlds.

actual    = [1.0, 1.1, 0.9, 0.5, 3.0]
normal    = [0.9, 2.0, 0.7, 0.3, 2.0]
outlier   = [0.9, 2.0, 0.7, 0.3, 100.0]

for name, pred in [("Normal", normal), ("Outlier", outlier)]:
    print(f"{name} predictions:")
    print(f"  MSE:   {compute_mse(actual, pred):.4f}")
    print(f"  MAE:   {compute_mae(actual, pred):.4f}")
    print(f"  Huber: {huber_loss(actual, pred, delta=1):.4f}")
    print()

What You Invented

Your Invention → Industry Name

Exercise What You Did Industry Name
1.1 Discovered that plain differences cancel out The need for a loss function
1.2 Found two ways to fix cancellation Squared error / Absolute error
2.1–2.2 Compared MSE and MAE, found drawbacks Bias–variance tradeoff of losses
3.1 Computed gradient of squared error MSE gradient: 2·diff
3.2 Computed gradient of absolute error MAE gradient: sign(diff)
3.3 Found MAE is not differentiable at 0 Non-differentiability of L1 loss
4.1–4.2 Tried naive combination, spotted discontinuity Piecewise loss design
4.3 Solved for A and B to make it smooth Huber loss derivation
5.1–5.3 Implemented and compared all three Huber loss (Huber, 1964)

Going further:

Glossary

Loss function
A function that takes actual and predicted values and returns a single number measuring how wrong the predictions are.
MSE (Mean Squared Error)
Average of squared differences between actual and predicted values. Penalises large errors heavily.
MAE (Mean Absolute Error)
Average of absolute differences between actual and predicted values. Treats all error sizes proportionally.
Gradient
The derivative of a function — tells you how much the output changes when you nudge the input by a tiny amount.
Differentiable
A function is differentiable at a point if it has a well-defined gradient there — no sharp corners or jumps.
Huber loss
A hybrid loss function: squared error for small mistakes, linear error for large ones. Smooth everywhere, robust to outliers.
Delta (δ)
The threshold in Huber loss that separates the quadratic region (small errors) from the linear region (large errors).
Outlier
A data point with an unusually large error — far from the rest. Outliers can distort loss functions like MSE.