First Example: Predicting House Prices (Regression)
The last two examples predicted a category. This one predicts a number — the median price of a house — which changes the output layer, the loss function, and ( because there’s so little data) how you validate the model at all.
The dataset: few samples, mismatched scales
This dataset has only 506 data points total — 404 for training, 102 for testing — each with 13 numerical features about a Boston-area suburb in the 1970s (crime rate, rooms per dwelling, distance to employment centers, and so on). Two things make this example different from IMDB and Reuters:
- Very little data — 404 samples is tiny by deep learning standards.
- Mismatched feature scales — one column might range 0–1, another 1–12, another 0–100.
from tensorflow.keras.datasets import boston_housing
(train_data, train_targets), (test_data, test_targets) = boston_housing.load_data()
print(train_data.shape) # (404, 13)
print(test_data.shape) # (102, 13)
print(train_targets[:5]) # e.g. [15.2, 42.3, 50.0, 21.1, 17.7] -- prices in $1000sfrom tensorflow.keras.datasets import boston_housing
(train_data, train_targets), (test_data, test_targets) = boston_housing.load_data()
print(train_data.shape) # (404, 13)
print(test_data.shape) # (102, 13)
print(train_targets[:5]) # e.g. [15.2, 42.3, 50.0, 21.1, 17.7] -- prices in $1000sPreparing the data: feature-wise normalization
Feeding a network features on wildly different scales makes learning needlessly hard. The standard fix: for each column, subtract its mean and divide by its standard deviation, so every feature ends up centered at 0 with a standard deviation of 1:
mean = train_data.mean(axis=0)
train_data -= mean
std = train_data.std(axis=0)
train_data /= std
test_data -= mean # reuse the TRAINING mean/std -- never compute stats from test data
test_data /= stdmean = train_data.mean(axis=0)
train_data -= mean
std = train_data.std(axis=0)
train_data /= std
test_data -= mean # reuse the TRAINING mean/std -- never compute stats from test data
test_data /= stdThe test set is normalized using the training set’s mean and standard deviation — using anything computed from the test data, even something this simple, leaks information you’re not supposed to have yet.
Building the model: a linear output layer
With so few samples, a small model helps avoid overfitting — two 64-unit hidden layers is plenty here:
from tensorflow import keras
from tensorflow.keras import layers
def build_model():
model = keras.Sequential([
layers.Dense(64, activation="relu"),
layers.Dense(64, activation="relu"),
layers.Dense(1), # no activation -- a purely linear output
])
model.compile(optimizer="rmsprop", loss="mse", metrics=["mae"])
return modelfrom tensorflow import keras
from tensorflow.keras import layers
def build_model():
model = keras.Sequential([
layers.Dense(64, activation="relu"),
layers.Dense(64, activation="relu"),
layers.Dense(1), # no activation -- a purely linear output
])
model.compile(optimizer="rmsprop", loss="mse", metrics=["mae"])
return model- The output layer has 1 unit and no activation function. Any activation (like
sigmoidsigmoid) would squash the output into a fixed range — but a house price can be any real number, so the last layer stays purely linear (dot(input, W) + bdot(input, W) + b). msemse(mean squared error) is the standard regression loss.maemae(mean absolute error) is tracked as a metric because it’s easy to interpret directly: an MAE of 2.5 here means predictions are off by about $2,500 on average.build_model()build_model()is wrapped in a function because K-fold validation, next, needs a fresh, identically-initialized model for every fold.
flowchart LR A["Hidden layers"] --> B["Dense(1, sigmoid)
binary probability"] A --> C["Dense(N, softmax)
class probabilities"] A --> D["Dense(1)
no activation -- any real value"]
Why K-fold validation
With only 404 training samples, a single train/validation split would leave a
validation set of maybe 100 examples — small enough that the validation score would
swing wildly depending on which 100 examples happened to land there. K-fold
cross-validation fixes this: split the data into kk chunks, train kk separate
models (each holding out a different chunk for validation), and average the results.
import numpy as np
k = 4
num_val_samples = len(train_data) // k
num_epochs = 100
all_scores = []
for i in range(k):
val_data = train_data[i * num_val_samples: (i + 1) * num_val_samples]
val_targets = train_targets[i * num_val_samples: (i + 1) * num_val_samples]
partial_train_data = np.concatenate(
[train_data[:i * num_val_samples], train_data[(i + 1) * num_val_samples:]], axis=0)
partial_train_targets = np.concatenate(
[train_targets[:i * num_val_samples], train_targets[(i + 1) * num_val_samples:]], axis=0)
model = build_model()
model.fit(partial_train_data, partial_train_targets,
epochs=num_epochs, batch_size=16, verbose=0)
val_mse, val_mae = model.evaluate(val_data, val_targets, verbose=0)
all_scores.append(val_mae)
print(all_scores) # e.g. [2.1, 3.1, 2.6, 2.4] -- scores vary per fold
print(np.mean(all_scores)) # ~2.55 -- a far more reliable estimate than any single foldimport numpy as np
k = 4
num_val_samples = len(train_data) // k
num_epochs = 100
all_scores = []
for i in range(k):
val_data = train_data[i * num_val_samples: (i + 1) * num_val_samples]
val_targets = train_targets[i * num_val_samples: (i + 1) * num_val_samples]
partial_train_data = np.concatenate(
[train_data[:i * num_val_samples], train_data[(i + 1) * num_val_samples:]], axis=0)
partial_train_targets = np.concatenate(
[train_targets[:i * num_val_samples], train_targets[(i + 1) * num_val_samples:]], axis=0)
model = build_model()
model.fit(partial_train_data, partial_train_targets,
epochs=num_epochs, batch_size=16, verbose=0)
val_mse, val_mae = model.evaluate(val_data, val_targets, verbose=0)
all_scores.append(val_mae)
print(all_scores) # e.g. [2.1, 3.1, 2.6, 2.4] -- scores vary per fold
print(np.mean(all_scores)) # ~2.55 -- a far more reliable estimate than any single foldIndividual folds can disagree by a dollar or more per house — the average across folds is what you should trust.
Choosing the number of epochs
Training longer (say 500 epochs) while recording the validation MAE at every epoch, for every fold, lets you plot the average validation MAE curve and read off where it stops improving:
all_mae_histories = []
for i in range(k):
# ... same fold split as above ...
model = build_model()
history = model.fit(partial_train_data, partial_train_targets,
validation_data=(val_data, val_targets),
epochs=500, batch_size=16, verbose=0)
all_mae_histories.append(history.history["val_mae"])
average_mae_history = [
np.mean([fold[epoch] for fold in all_mae_histories]) for epoch in range(500)
]all_mae_histories = []
for i in range(k):
# ... same fold split as above ...
model = build_model()
history = model.fit(partial_train_data, partial_train_targets,
validation_data=(val_data, val_targets),
epochs=500, batch_size=16, verbose=0)
all_mae_histories.append(history.history["val_mae"])
average_mae_history = [
np.mean([fold[epoch] for fold in all_mae_histories]) for epoch in range(500)
]In the book, this curve flattens out around epoch 120–140 — training past that point just overfits.
Training the final model
Once you’ve picked a good number of epochs, train one last model on all the training data and check it against the test set:
model = build_model()
model.fit(train_data, train_targets, epochs=130, batch_size=16, verbose=0)
test_mse_score, test_mae_score = model.evaluate(test_data, test_targets)
print(test_mae_score) # roughly 2.5 -- off by about $2,500 on averagemodel = build_model()
model.fit(train_data, train_targets, epochs=130, batch_size=16, verbose=0)
test_mse_score, test_mae_score = model.evaluate(test_data, test_targets)
print(test_mae_score) # roughly 2.5 -- off by about $2,500 on averagepredictions = model.predict(test_data)
print(predictions[0]) # e.g. array([9.99], dtype=float32) -- ~$10,000predictions = model.predict(test_data)
print(predictions[0]) # e.g. array([9.99], dtype=float32) -- ~$10,000Mini-checkpoint
Why does the model’s final DenseDense layer have no activation function?
- Applying an activation like
sigmoidsigmoidorsoftmaxsoftmaxwould constrain the output to a fixed range (like 0–1). A house price can be any real number, so the last layer stays a plain linear transformation.
Next
You’ve now built all three of the classic vector-data workflows — binary classification, multiclass classification, and regression. Continue to Phase 2: Training Deep Neural Networks to see how backpropagation, optimizers, and regularization scale these same ideas up to bigger, deeper networks.
🧪 Try It Yourself
Exercise 1 – Normalize Features by Hand
Exercise 2 – A Linear Output Layer for Regression
Exercise 3 – Average K-Fold Scores
If this helped you, consider buying me a coffee ☕
Buy me a coffeeWas this page helpful?
Let us know how we did
