Measuring Overfitting
The gap that matters, and the technique that beats all the others
A network with more parameters than examples can fit the training set exactly, including whatever is accidental about it. Every technique this week is a way of making that harder without making the useful fitting harder too.
The problem, measured
from torch import nn
from torch.utils.data import DataLoader, Subset
from torchvision import datasets, transforms
tf = transforms.Compose([transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))])
train_all = datasets.MNIST('data', train=True, download=True, transform=tf)
# A small training set, so overfitting is easy to produce and easy to fix.
train_set = Subset(train_all, range(1000))
val_set = Subset(train_all, range(10000, 12000))
def build(dropout=0.0, hidden=512, seed=0):
torch.manual_seed(seed)
return nn.Sequential(
nn.Flatten(),
nn.Linear(784, hidden), nn.ReLU(), nn.Dropout(dropout),
nn.Linear(hidden, hidden), nn.ReLU(), nn.Dropout(dropout),
nn.Linear(hidden, 10))
def run(model, epochs=30, wd=0.0, label_smoothing=0.0, train=None,
seed=0, lr=0.05):
torch.manual_seed(seed)
loader = DataLoader(train if train is not None else train_set,
batch_size=64, shuffle=True)
val = DataLoader(val_set, batch_size=512)
opt = torch.optim.SGD(model.parameters(), lr=lr, momentum=0.9,
weight_decay=wd)
loss_fn = nn.CrossEntropyLoss(label_smoothing=label_smoothing)
for _ in range(epochs):
model.train()
for xb, yb in loader:
opt.zero_grad()
loss_fn(model(xb), yb).backward()
opt.step()
model.eval()
plain = nn.CrossEntropyLoss()
out = []
for name, dl in [('train', DataLoader(train if train is not None
else train_set, batch_size=512)),
('val', val)]:
right = seen = 0
with torch.no_grad():
for xb, yb in dl:
right += (model(xb).argmax(1) == yb).sum().item()
seen += yb.numel()
out.append(right / seen)
return out
model = build(dropout=0.0)
print('parameters %d, training examples %d'
% (sum(p.numel() for p in model.parameters()), len(train_set)))
train_acc, val_acc = run(model)
print('\ntrain accuracy %.4f' % train_acc)
print('val accuracy %.4f' % val_acc)
print('gap %.4f' % (train_acc - val_acc))
train accuracy 1.0000
val accuracy 0.8920
gap 0.1080
The gap is the thing to watch, not the validation number
A validation accuracy that is lower than training accuracy is normal. A validation accuracy far lower, with training at or near 1.000, means the model has capacity it is spending on memorisation. That is a fixable situation and this week is the toolbox. A model where both numbers are low has the opposite problem and none of these techniques will help it.
More data is the technique that always works
from torch import nn
from torch.utils.data import DataLoader, Subset
from torchvision import datasets, transforms
tf = transforms.Compose([transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))])
train_all = datasets.MNIST('data', train=True, download=True, transform=tf)
# A small training set, so overfitting is easy to produce and easy to fix.
train_set = Subset(train_all, range(1000))
val_set = Subset(train_all, range(10000, 12000))
def build(dropout=0.0, hidden=512, seed=0):
torch.manual_seed(seed)
return nn.Sequential(
nn.Flatten(),
nn.Linear(784, hidden), nn.ReLU(), nn.Dropout(dropout),
nn.Linear(hidden, hidden), nn.ReLU(), nn.Dropout(dropout),
nn.Linear(hidden, 10))
def run(model, epochs=30, wd=0.0, label_smoothing=0.0, train=None,
seed=0, lr=0.05):
torch.manual_seed(seed)
loader = DataLoader(train if train is not None else train_set,
batch_size=64, shuffle=True)
val = DataLoader(val_set, batch_size=512)
opt = torch.optim.SGD(model.parameters(), lr=lr, momentum=0.9,
weight_decay=wd)
loss_fn = nn.CrossEntropyLoss(label_smoothing=label_smoothing)
for _ in range(epochs):
model.train()
for xb, yb in loader:
opt.zero_grad()
loss_fn(model(xb), yb).backward()
opt.step()
model.eval()
plain = nn.CrossEntropyLoss()
out = []
for name, dl in [('train', DataLoader(train if train is not None
else train_set, batch_size=512)),
('val', val)]:
right = seen = 0
with torch.no_grad():
for xb, yb in dl:
right += (model(xb).argmax(1) == yb).sum().item()
seen += yb.numel()
out.append(right / seen)
return out
print('%10s %12s %12s %10s' % ('examples', 'train acc', 'val acc', 'gap'))
for n in [250, 1000, 4000, 16000]:
subset = Subset(train_all, range(n))
tr, va = run(build(), epochs=12, train=subset)
print('%10d %12.4f %12.4f %10.4f' % (n, tr, va, tr - va))
250 1.0000 0.7920 0.2080
1000 1.0000 0.8875 0.1125
4000 1.0000 0.9605 0.0395
16000 0.9996 0.9990 0.0006
Nothing else on this page competes with that column. Every regularisation technique is an attempt to buy some of the same effect when you cannot get more data, and they all buy less of it than the data itself would. It is worth remembering when a week of tuning is on the table and a day of labelling is also on the table.