As an addition to the answers already given, I conducted a small experiment using the MNIST dataset:
- train/val split: 60000/10000
- input dimensions: 784
- hidden layers: 1
- output neurons: 10
The number of neurons in the hidden layer is what I used to adjust the model complexity.
import matplotlib.pyplot as plt
import numpy as np
import tensorflow as tf
import tensorflow_datasets as tfds
def normalize_img(image, label):
"""Normalizes images: `uint8` -> `float32`."""
return tf.cast(image, tf.float32) / 255., label
(ds_train, ds_test), ds_info = tfds.load(
"mnist",
split=["train", "test"],
shuffle_files=True,
as_supervised=True,
with_info=True,
)
# 60000
ds_train = ds_train.map(normalize_img, num_parallel_calls=tf.data.AUTOTUNE)
ds_train = ds_train.cache()
ds_train = ds_train.shuffle(ds_info.splits["train"].num_examples)
ds_train = ds_train.batch(128)
ds_train = ds_train.prefetch(tf.data.AUTOTUNE)
# 10000
ds_test = ds_test.map(normalize_img, num_parallel_calls=tf.data.AUTOTUNE)
ds_test = ds_test.batch(128)
ds_test = ds_test.cache()
ds_test = ds_test.prefetch(tf.data.AUTOTUNE)
for hidden_layer in range(2, 21):
# https://www.tensorflow.org/datasets/keras_example
model = tf.keras.models.Sequential([
tf.keras.layers.Flatten(input_shape=(28, 28)),
tf.keras.layers.Dense(hidden_layer, activation='elu'),
tf.keras.layers.Dense(10)
])
model.compile(
optimizer=tf.keras.optimizers.Adam(0.001),
loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
metrics=[tf.keras.metrics.SparseCategoricalAccuracy()],
)
model.summary()
history = model.fit(ds_train, epochs=100, validation_data=ds_test)
layers = [28 * 28, hidden_layer, 10]
params = model.count_params()
val_loss = history.history["val_loss"]
min_val_loss = min(val_loss)
min_val_loss_min_ep = np.argmin(val_loss)
min_val_loss_max_ep = len(val_loss) - np.argmin(val_loss[::-1]) - 1
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.plot(history.epoch, history.history["loss"], label="train")
ax.plot(history.epoch, history.history["val_loss"], label="val")
ax.set_title(f"{layers=}; {params=}; {min_val_loss=:8.6};\n{min_val_loss_min_ep=}; {min_val_loss_max_ep=}")
ax.set_ylim([0, 1.2])
plt.xlabel("epochs")
plt.ylabel("loss")
plt.legend(loc="upper center")
plt.grid()
fig.savefig(f"{hidden_layer:02d}.png")
Resulting learning curves: https://imgur.com/a/SskHmNC
Example with 10 hidden neurons (does not need early stopping):
Example with 19 hidden neurons (does need early stopping around epoch 20):
Interpretation: From some number of neurons in the hidden layer upward, we need early stopping to get the best validation loss for these models.
While I was somewhat cheering for the fewer-neurons solution to win, in this experiment, the solutions with more neurons (and early stopping!) performed better, i.e., the best validation loss was significantly lower.