Week 8 promised a model that works out its own kernels rather than using the ones a person chose. Here it is, and it is a short piece of code.
import numpy as np
import torch
from torch import nn
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
digits = load_digits()
images, y = digits.images / 16.0, digits.target
flat = images.reshape(len(images), -1)
idx = np.arange(len(y))
tr, te = train_test_split(idx, test_size=0.3, random_state=0, stratify=y)
RAMP = ' .:-=+*#%@'
def show(img):
for row in img:
print(''.join(RAMP[min(9, max(0, int(v * 9.999)))] for v in row))
def train_cnn(epochs=30, seed=0, augment=False, trace_every=0):
torch.manual_seed(seed)
model = nn.Sequential(
nn.Conv2d(1, 8, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2),
nn.Conv2d(8, 16, 3, padding=1), nn.ReLU(),
nn.Flatten(), nn.Linear(16 * 4 * 4, 10))
opt = torch.optim.Adam(model.parameters(), lr=3e-3)
lf = nn.CrossEntropyLoss()
xb = torch.tensor(images[tr], dtype=torch.float32).unsqueeze(1)
yb = torch.tensor(y[tr])
for ep in range(1, epochs + 1):
model.train()
batch = xb
if augment:
shifts = torch.randint(-1, 2, (2,))
batch = torch.roll(xb, (int(shifts[0]), int(shifts[1])),
dims=(2, 3))
opt.zero_grad()
loss = lf(model(batch), yb)
loss.backward()
opt.step()
if trace_every and ep % trace_every == 0:
print('%6d %12.4f' % (ep, loss.item()))
model.eval()
return model
@torch.no_grad()
def cnn_score(model, imgs, labels):
x = torch.tensor(imgs, dtype=torch.float32).unsqueeze(1)
return float((model(x).argmax(1).numpy() == labels).mean())
model = train_cnn(epochs=1)
params = sum(p.numel() for p in model.parameters())
print(model)
print()
print('%d parameters, of which the first layer holds %d'
% (params, 8 * 1 * 3 * 3 + 8))
Sequential(
(0): Conv2d(1, 8, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(1): ReLU()
(2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(3): Conv2d(8, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(4): ReLU()
(5): Flatten(start_dim=1, end_dim=-1)
(6): Linear(in_features=256, out_features=10, bias=True)
)
3818 parameters, of which the first layer holds 80
The first layer is eight kernels of three by three, exactly the shape of the edge detector written by hand last week. The difference is that these eighty-one numbers start random and are adjusted by training.
import numpy as np
import torch
from torch import nn
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
digits = load_digits()
images, y = digits.images / 16.0, digits.target
flat = images.reshape(len(images), -1)
idx = np.arange(len(y))
tr, te = train_test_split(idx, test_size=0.3, random_state=0, stratify=y)
RAMP = ' .:-=+*#%@'
def show(img):
for row in img:
print(''.join(RAMP[min(9, max(0, int(v * 9.999)))] for v in row))
def train_cnn(epochs=30, seed=0, augment=False, trace_every=0):
torch.manual_seed(seed)
model = nn.Sequential(
nn.Conv2d(1, 8, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2),
nn.Conv2d(8, 16, 3, padding=1), nn.ReLU(),
nn.Flatten(), nn.Linear(16 * 4 * 4, 10))
opt = torch.optim.Adam(model.parameters(), lr=3e-3)
lf = nn.CrossEntropyLoss()
xb = torch.tensor(images[tr], dtype=torch.float32).unsqueeze(1)
yb = torch.tensor(y[tr])
for ep in range(1, epochs + 1):
model.train()
batch = xb
if augment:
shifts = torch.randint(-1, 2, (2,))
batch = torch.roll(xb, (int(shifts[0]), int(shifts[1])),
dims=(2, 3))
opt.zero_grad()
loss = lf(model(batch), yb)
loss.backward()
opt.step()
if trace_every and ep % trace_every == 0:
print('%6d %12.4f' % (ep, loss.item()))
model.eval()
return model
@torch.no_grad()
def cnn_score(model, imgs, labels):
x = torch.tensor(imgs, dtype=torch.float32).unsqueeze(1)
return float((model(x).argmax(1).numpy() == labels).mean())
print('%6s %12s' % ('epoch', 'loss'))
model = train_cnn(epochs=30, trace_every=5)
print()
print('accuracy on held out images %.4f'
% cnn_score(model, images[te], y[te]))
epoch loss
5 2.2540
10 2.1439
15 1.9455
20 1.6400
25 1.2500
30 0.8662
accuracy on held out images 0.8630
What it learned to look for
import numpy as np
import torch
from torch import nn
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
digits = load_digits()
images, y = digits.images / 16.0, digits.target
flat = images.reshape(len(images), -1)
idx = np.arange(len(y))
tr, te = train_test_split(idx, test_size=0.3, random_state=0, stratify=y)
RAMP = ' .:-=+*#%@'
def show(img):
for row in img:
print(''.join(RAMP[min(9, max(0, int(v * 9.999)))] for v in row))
def train_cnn(epochs=30, seed=0, augment=False, trace_every=0):
torch.manual_seed(seed)
model = nn.Sequential(
nn.Conv2d(1, 8, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2),
nn.Conv2d(8, 16, 3, padding=1), nn.ReLU(),
nn.Flatten(), nn.Linear(16 * 4 * 4, 10))
opt = torch.optim.Adam(model.parameters(), lr=3e-3)
lf = nn.CrossEntropyLoss()
xb = torch.tensor(images[tr], dtype=torch.float32).unsqueeze(1)
yb = torch.tensor(y[tr])
for ep in range(1, epochs + 1):
model.train()
batch = xb
if augment:
shifts = torch.randint(-1, 2, (2,))
batch = torch.roll(xb, (int(shifts[0]), int(shifts[1])),
dims=(2, 3))
opt.zero_grad()
loss = lf(model(batch), yb)
loss.backward()
opt.step()
if trace_every and ep % trace_every == 0:
print('%6d %12.4f' % (ep, loss.item()))
model.eval()
return model
@torch.no_grad()
def cnn_score(model, imgs, labels):
x = torch.tensor(imgs, dtype=torch.float32).unsqueeze(1)
return float((model(x).argmax(1).numpy() == labels).mean())
model = train_cnn(epochs=30)
kernels = model[0].weight.detach().numpy()[:, 0]
for i in range(3):
k = kernels[i]
print('kernel %d:' % (i + 1))
for row in k:
print(' ' + ' '.join('%6.2f' % v for v in row))
print()
kernel 1:
0.01 0.17 -0.34
-0.34 -0.19 0.12
-0.09 0.36 0.07
kernel 2:
0.06 -0.17 -0.14
-0.39 -0.30 -0.20
-0.05 0.23 0.30
kernel 3:
-0.31 -0.24 0.20
0.28 0.02 0.33
0.04 0.13 0.39
Nobody chose those numbers. Compare them with week 8's edge detector, which had a negative column, a zero column and a positive column: several of these have a similar structure, arrived at from data alone because that is what is useful for telling digits apart.