Index Ai Academy
Phase 3 · Machine learning · Evaluation

Overfitting and underfitting

Memorizing versus learning.

About 25 minutes · A free account opens the whole lesson

Create a free account Try part of it here

Already have an account? Sign in to open this lesson

Why it matters

Overfitting is the central problem of machine learning. A model overfits when it learns the noise in its training data instead of the real pattern. It's why you hold data back, and why regularization exists. It's also why "it worked great in testing" so often fails once a model meets real customers. LLM work fails the same way. You can tweak a prompt until every eval example passes, or fine-tune a model until it parrots its training set.

What you'll be able to do

  • Recognize underfitting and overfitting from training and validation scores
  • Draw and read a model-complexity curve
  • Use the validation set to choose complexity
  • List the fixes: more data, simpler models, regularization, early stopping

What it covers

  1. Memorizing versus learning
  2. The complexity curve
  3. What to do about overfitting
  4. Worked example: the memorizer and the rule
  5. The same problem with prompts and LLMs

Memorizing versus learning

Picture two students getting ready for a math exam. The first one barely studies. They know a few facts but not how to solve the problems. They do badly on the practice tests and badly on the real exam.

The second student finds last year's exam and learns every answer by heart. On last year's questions they score 100%. On the real exam the questions are new, and they fall apart. They learned the answers, not the method.

Models can fail in both of these ways. Each one has a name.

  • Underfitting is the first student. The model is too simple to capture the real pattern. It does poorly on the training data and on new data. Think of a straight line drawn through points that follow a curve.
  • Overfitting is the second student. The model is so flexible that it fits the noise too. It does great on the training data and poorly on new data. Think of a line that wiggles to pass through every single point.

Noise means the random quirks in your data that won't repeat. Say a loyal customer cancels because they moved abroad. Nothing in the data could predict that. A model that bends itself to explain that one customer has learned something false.

Read the gap between two scores

You met these two kinds of data in the lesson on train, validation and test sets. The training score is the model's accuracy on the rows it learned from. The validation score is its accuracy on held-back rows it never saw. Put the two side by side and the diagnosis is usually clear:

Training scoreValidation scoreDiagnosis
lowlowunderfitting: more flexible model, better features
highabout as highgood fit
very highmuch loweroverfitting: simpler model, regularization, more data

The key sign of overfitting is the gap: the training score minus the validation score. A small gap means the model does about as well on new data as on old. A big gap means it memorized.

Quick check
A model scores 0.97 on training data and 0.71 on validation data. What's the most likely diagnosis?
  1. A data loading bug, certainly
  2. A good fit
  3. Underfitting
  4. Overfitting
Show the answer

Overfitting

The gap is 0.97 − 0.71 = 0.26. A large gap between training and validation scores is the signature of overfitting.

The complexity curve

How flexible a model is gets called its complexity. A simple model can only draw broad patterns. A complex one can bend to fit tiny details, including noise.

A decision tree makes this easy to see. A tree learns a flowchart of yes/no questions. Its depth is the longest chain of questions it may ask before it gives an answer. A tree of depth 1 asks one question. A tree of depth 15 can ask 15 in a row, so it can split the customers into very small groups.

So here's the experiment. Train trees of depth 1, 2, 3 and so on, up to 15. For each one, record the training score and the validation score. Then plot both against depth. That plot is called a complexity curve.

The code below does this on the churn data from earlier lessons. (A customer churned if they stopped buying.) It keeps 30% of customers back for validation. It draws both curves and puts a dashed gray line at the depth with the best validation score. Then it prints that depth and its score.

import numpy as np
import pandas as pd
import requests
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier

df = pd.DataFrame(requests.get("https://api.gradient.test/v1/customers").json()["data"])
df["email_open_rate"] = df["email_open_rate"].fillna(0)
features = ["tenure_months", "orders_last_year", "avg_order_value", "days_since_last_order",
            "discount_share", "email_open_rate", "support_tickets", "trail_club"]
X_tr, X_val, y_tr, y_val = train_test_split(df[features], df["churned"], test_size=0.3, random_state=0, stratify=df["churned"])

depths = range(1, 16)
train_acc, val_acc = [], []
for d in depths:
    tree = DecisionTreeClassifier(max_depth=d, random_state=0).fit(X_tr, y_tr)
    train_acc.append(tree.score(X_tr, y_tr))
    val_acc.append(tree.score(X_val, y_val))

best = depths[int(np.argmax(val_acc))]
plt.plot(depths, train_acc, marker="o", label="training")
plt.plot(depths, val_acc, marker="o", label="validation")
plt.axvline(best, linestyle="--", color="gray")
plt.xlabel("max tree depth (complexity)")
plt.ylabel("accuracy")
plt.legend()
print("best depth by validation:", best, " validation accuracy:", round(max(val_acc), 3))
It runs right here in your browser, with nothing to install.

Look at the two lines. Training accuracy climbs steadily toward 1.0, which means perfect. The deeper the tree, the better it fits the customers it learned from.

Validation accuracy tells a different story. It rises a little, peaks at a shallow depth, and then falls. Past the peak, each extra level lets the tree memorize more quirks of the training customers. Those quirks don't carry over to new customers, so the validation score drops.

The best model sits where validation peaks, not where training does. That's the depth you should pick.

Go deeper Why deep trees can memorize

Each extra level of a tree can double the number of end groups, called leaves. Depth 1 has at most 2 leaves. Depth 2 has at most 4, depth 10 at most 1,024, and depth 15 at most 32,768. With that much room, the tree can give tiny groups of one or two customers a leaf of their own. Each leaf then repeats whatever those few customers did, noise included. That is memorizing, one leaf at a time.

Try it now

This is the complexity curve from the section above: training error keeps dropping, and error on new data turns back up.

Quick check
Here are the scores from five trees of depth 1 to 5. What does this print?
train_acc = [0.81, 0.84, 0.88, 0.93, 0.99]
val_acc = [0.80, 0.83, 0.82, 0.78, 0.74]
depths = [1, 2, 3, 4, 5]
print(depths[val_acc.index(max(val_acc))])
  1. 1
  2. 5
  3. 2
  4. 3
Show the answer

2

The highest validation score is 0.83, at depth 2. Depth 5 has the best training score, but its validation score has fallen to 0.74: it's overfitting.

What to do about overfitting

There are five standard fixes. Each one either gives the model more to learn from or makes it harder to memorize.

  1. More data. This is the single most reliable fix. Noise averages out across many examples. The real pattern doesn't, so it stands out more clearly.
  2. A simpler model. Use shallower trees or fewer features. In nearest neighbors, which predicts from the most similar past examples, ask more neighbors (a larger k). One odd neighbor then matters less.
  3. Regularization. Give the model a penalty for being complicated, so it only adds detail that pays off. That's the next lesson.
  4. Ensembles. Train many models and average their answers. Each overfit tree makes its own random mistakes, and averaging cancels many of them out. A random forest works this way. The lesson on random forests and boosting covers it.
  5. Early stopping. Some models learn step by step, in passes over the training data. Watch the validation score as they train. Stop when it stops improving.

Underfitting needs the opposite medicine. Try a more flexible model, better features, less regularization, or longer training.

Quick check
Your tree with no depth limit scores 0.99 on training data and 0.70 on validation data. Which change should you try first?
  1. Add more features, including some random ones
  2. Report the 0.99, since it's the higher score
  3. Limit the depth so the tree is simpler
  4. Remove the depth limit entirely, so the tree can grow deeper
Show the answer

Limit the depth so the tree is simpler

The big gap says the tree is overfitting. A simpler model gives it less room to memorize. The other options add flexibility or hide the problem.

Worked example

Worked example: the memorizer and the rule

Here are 8 training customers and 6 validation customers. For each one you know the number of days since their last order, and whether they churned (1) or stayed (0). Two of the training customers are noise. One churned after only 33 days. One stayed after 90 days.

Compare two models. The memorizer learns every training customer by heart. For a customer it hasn't seen, it guesses "stays". The rule says: churn if there's been no order for more than 60 days. Before you run it, guess which one has the bigger gap.

train = [(5, 0), (12, 0), (20, 0), (33, 1), (45, 0), (70, 1), (85, 1), (90, 0)]
val = [(8, 0), (25, 0), (50, 1), (65, 1), (80, 1), (95, 1)]

memory = dict(train)                    # learn every training customer by heart

def memorizer(days):
    return memory.get(days, 0)          # a customer it hasn't seen: guess "stays" (0)

def rule(days):
    return 1 if days > 60 else 0        # churn if no order for more than 60 days

def accuracy(model, rows):
    return sum(model(days) == label for days, label in rows) / len(rows)

for name, model in [("memorizer", memorizer), ("rule", rule)]:
    tr, va = accuracy(model, train), accuracy(model, val)
    print(f"{name:<10} train {tr:.2f}  val {va:.2f}  gap {tr - va:.2f}")
In the lesson you can change this and run it.

The memorizer gets all 8 training customers right, so it scores 1.00. On validation it has nothing to go on, so it says "stays" every time. That's right for only 2 of 6 customers, 0.33. Its gap is 0.67: textbook overfitting.

The rule misses the two noisy training customers, so it scores 6 of 8, or 0.75. On validation it gets 5 of 6 right, or 0.83. Its gap is −0.08, so it does no worse on new customers. The rule looks worse on training data and is the better model.

The same problem with prompts and LLMs

Overfitting isn't only about trees or numbers. It shows up whenever you tune something on a fixed set of examples.

  • Prompts. Say you edit a prompt until all 30 of your eval examples pass. Each fix targets one failing case. In the end the prompt handles those 30 well and may act oddly on the 31st. That's overfitting to those 30 examples. Keep a held-out set that you only check at the end (Phase 1's evals lesson).
  • Fine-tuning. Fine-tuning means training an existing model further on your own small dataset. Train for too many passes and the model starts repeating its training examples. It also loses some of its general skill. A rising validation loss, the error on held-back examples, is the warning sign (Phase 6).

For marketers: say you "optimize" a campaign on last month's data, down to the exact hour, audience and phrasing. It may be overfit to last month. Patterns that carry over are usually simpler than the ones that fit history perfectly.

Quick check
You've tweaked a prompt until all 30 eval examples pass. How do you find out whether the improvement is real?
  1. Run the same 30 examples again
  2. Add a special-case instruction for any example that still fails
  3. Ask the model whether the prompt is good
  4. Test it on held-out examples you didn't look at while editing
Show the answer

Test it on held-out examples you didn't look at while editing

Passing the examples you tuned on proves little, like a training score. Only examples kept out of the editing show how the prompt handles new inputs.

Practice what you just read

  • 2 code examples you can edit and run
  • a practice exercise that checks your code
  • a 5-question quiz
  • 4 review cards for spaced repetition

Create a free account

Try a quiz question

Training accuracy 0.62, validation accuracy 0.60, majority baseline 0.65. What's happening?

  1. Underfitting: the model hasn't even learned the training data
  2. Overfitting: the model has memorized the training data
  3. A near-perfect fit: the two scores are almost the same
  4. Data leakage: the validation set has leaked into training

Answer it at the end of the lesson to see if you're right, and why.

Before this lesson