fastai - Lesson 4 - Deep Learning for Coders (2020)
Notes taken from the Deep Learning for Coders (2020) - Lesson 4 video
Lex Toumbourou ·
Notes taken from watching the Lesson 4 - Deep Learning for Coders (2020) video.
00:00:00 - Lesson 3 review
- Lesson 4 is going to take a deeper dive into training neural networks.
- Original MNIST implementation wasn't based on Arthur Samuel's idea to "altering the weight assignment to maximise performance"
- Stochasic Gradient Descent (SGD) review
- Seven steps:
- Init weights
- Make prediction
- Calculate loss
- Calculate gradients
- Take a step in the direction of the gradient
- Stop when good enough
- Seven steps:
00:03:16 - MNIST loss function
from fastai.vision.all import *
path = untar_data(URLs.MNIST_SAMPLE)
threes = (path/'train'/'3').ls().sorted()
sevens = (path/'train'/'7').ls().sorted()
seven_tensors = [tensor(Image.open(o)) for o in sevens]
three_tensors = [tensor(Image.open(o)) for o in threes]
stacked_sevens = torch.stack(seven_tensors).float() / 255
stacked_threes = torch.stack(three_tensors).float() / 255
valid_3_tens = torch.stack([tensor(Image.open(o))
for o in (path/'valid'/'3').ls()])
valid_3_tens = valid_3_tens.float()/255
valid_7_tens = torch.stack([tensor(Image.open(o))
for o in (path/'valid'/'7').ls()])
valid_7_tens = valid_3_tens.float()/255
- Concat
xs into a single tensor and change from list of matrices (rank 3 tensor) to list of vectors (rank 2 tensor)viewmethod lets you change shape of tensor.-1is special param: make axis as big as needed to fill data.
train_x = torch.cat([stacked_threes, stacked_sevens]).view(-1, 28 * 28)
- Need a label for each
1for threes and0for sevens:- Adds an additional unit dimension to the position passed.
train_y = tensor([1] * len(threes) + [0]*len(sevens)).unsqueeze(1)
train_x.shape, train_y.shape
(torch.Size([12396, 784]), torch.Size([12396, 1]))
- A [[PyTorch Dataset]] can be indexed into:
[i]which should return a tuple.
dset = list(zip(train_x, train_y))
x, y = dset[0]
x.shape, y
(torch.Size([784]), tensor([1]))
- Create out validation set:
valid_x = torch.cat([valid_3_tens, valid_7_tens]).view(-1, 28 * 28)
valid_y = tensor([1]*len(valid_3_tens) + [0]*len(valid_7_tens)).unsqueeze(1)
valid_dset = list(zip(valid_x, valid_y))
00:07:46 - Initialise params
- Need to set initially random weight for every pixel:
- It has a variance of 1, so we can multiply by variance.
def init_params(size, variance=1.0):
return (torch.randn(size)*variance).requires_grad_()
- Weights are 28*28, 1 (a flat vector of pixels)
weights = init_params(28*28, 1)
00:09:39 - Weights and biases
weights*pixelsisn't flexible enough, it is always equal to zero when pixels are zero (ie intercept is zeo)- Need the bias term in slope:
y=w*x+b - Init to random number here:
- Need the bias term in slope:
bias = init_params(1)
- Weights and bias make up the parameters of a model
- Can calculate prediction for one image:
- Need to first transpose them to line them up:
train_x[0].shape, weights.T.shape
(torch.Size([784]), torch.Size([784]))
(train_x[0] * weights.T).sum() + bias
tensor([11.7973], grad_fn=<AddBackward0>)
00:10:52 - calculate predictions with matrix multiplication
- Have to run that for every image.
- Could run a for loop, but it would be slow and couldn't use GPU.
- Doing that operation for multiple rows and cols is a matrix multiplication. In Python, matrix multiplication uses
@symbol
def linear1(xb):
return xb@weights + bias
preds = linear1(train_x)
preds
tensor([11.7973, 3.0019, 4.1781, ..., -2.5707, 3.1603, 3.3594],
grad_fn=<AddBackward0>)
- That is one of the [[Two fundamental equations of a neural network]]. Activation function is the other.
00:14:28 - Why accuracy isn't a loss function
- Check accuuracy by checking if it's greater than 0 using broadcasting.
corrects = (preds>0.0).float() == train_y
corrects
tensor([[ True, True, True, ..., False, True, True],
[ True, True, True, ..., False, True, True],
[ True, True, True, ..., False, True, True],
...,
[False, False, False, ..., True, False, False],
[False, False, False, ..., True, False, False],
[False, False, False, ..., True, False, False]])
item()returns a normal Python scalar, not a tensor.
corrects.float().mean().item()
0.49700450897216797
- See what happens to accuracy with small change in weights:
weights[0] *= 1.001
preds = linear1(train_x)
((preds>0.0).float() == train_y).float().mean().item()
0.49700450897216797
- Since gradient is equal to rise over run: how much value of function goes up or down when divided by change in input.
- Can write in maths:
(y_new - y_old) / (x_new - x_old) - Accuracy only changes when a prediction goes from 3 to 7.
- Small change in weights from
x_oldtox_newisn't likely to change prediction, so gradient is always 0.
- Can write in maths:
- If gradient is 0, then step will be 0.
00:18:40 - A better loss function
- New function should give a better value when predictions are closer to ground truth, but never with zero gradient.
trgts = tensor([1, 0, 1])
prds = tensor([0.9, 0.4, 0.2])
- Using
wherefunction:- For each value, if it matches the first condition, then set the 2nd argument, else 3rd.
- Works on tensors and runs at C/Cuda speed.
- Can then take the mean to calculate results.
def mnist_loss(predictions, targets):
return torch.where(targets==1, 1-predictions, predictions).mean()
mnist_loss(prds, trgts)
tensor(0.4333)
- If you change one of the predictions, you can see the loss improving:
prds = tensor([0.9, 0.4, 0.35])
mnist_loss(prds, trgts)
tensor(0.3833)
00:22:46 - Sigmoid
- The last problem with mnist_loss is it assumes preds are between zero and one.
- To ensure that use [[Sigmoid]].
- Take big numbers and turn into something between 0 and 1.
- To ensure that use [[Sigmoid]].
torch.exp(n)ismath.e^n
torch.exp(tensor(2.)), math.e**2
(tensor(7.3891), 7.3890560989306495)
def sigmoid(x):
return 1/(1+torch.exp(-x))
def plot_function(f, tx=None, ty=None, title=None, min=-2, max=2, figsize=(6,4)):
x = torch.linspace(min,max)
fig,ax = plt.subplots(figsize=figsize)
ax.plot(x,f(x))
if tx is not None: ax.set_xlabel(tx)
if ty is not None: ax.set_ylabel(ty)
if title is not None: ax.set_title(title)
- The definition of the function isn't that useful. The shape is useful.
- Torch already has a sigmoid function.
plot_function(torch.sigmoid, title='Sigmoid', min=-4, max=4)
/opt/conda/lib/python3.7/site-packages/ipykernel_launcher.py:2: UserWarning: Not providing a value for linspace's steps is deprecated and will throw a runtime error in a future release. This warning will appear only once per process. (Triggered internally at /opt/conda/conda-bld/pytorch_1603729047590/work/aten/src/ATen/native/RangeFactories.cpp:23.)
- Can change mnist_loss to be the same but changes to sigmoid first:
def mnist_loss(predictions, targets):
predictions = predictions.sigmoid()
return torch.where(targets==1, 1-predictions, predictions).mean()
- Now we have a loss function with no zero gradients and it's between 0 and 1.
00:27:12 - Updating parameters and mini-batches
- How you would update parameters without linear algebra:
- Loop through each image.
- Make a prediction.
- Calculate loss.
- Do step.
- Update parameters.
- Will be really slow - a single epoch woud take a while.
- Loop through each image.
- Could go faster by doing every image in the dataset with one big matrix multiplication.
- A lot of work to update weights once.
- Comprise: do a few at a time: a [[Mini-batch]]
- The bigger the batch size, the more "accurate" your gradient is.
- The smaller the batch size, the more you can updated your parameters.
00:30:50
- PyTorch have a class called
DataLoaderthat can give you data in a mini-batch.- Takes any Python collection and turns into iterator.
from fastai.data.load import DataLoader
col1 = range(15)
dl = DataLoader(col1, batch_size=5, shuffle=True)
list(dl)
[tensor([ 0, 5, 4, 12, 3]), tensor([10, 13, 6, 9, 7]), tensor([ 1, 8, 11, 2, 14])]
dl = DataLoader(col1, batch_size=10, shuffle=True)
list(dl)
[tensor([ 1, 9, 13, 11, 12, 2, 6, 10, 3, 8]), tensor([ 5, 7, 14, 0, 4])]
- When training model, you want collection containing independant and dependent variables (input and targets).
- A collection that has tuples of independent and dependent variables is known as Dataset in PyTorch.
import string
ds = L(enumerate(string.ascii_lowercase))
ds
(#26) [(0, 'a'),(1, 'b'),(2, 'c'),(3, 'd'),(4, 'e'),(5, 'f'),(6, 'g'),(7, 'h'),(8, 'i'),(9, 'j')...]
- Pass that to dataloader, see it returns tuples:
dl = DataLoader(ds, batch_size=6, shuffle=True)
list(dl)
[(tensor([17, 13, 15, 23, 19, 11]), ('r', 'n', 'p', 'x', 't', 'l')),
(tensor([16, 25, 12, 4, 14, 20]), ('q', 'z', 'm', 'e', 'o', 'u')),
(tensor([24, 18, 2, 22, 6, 21]), ('y', 's', 'c', 'w', 'g', 'v')),
(tensor([ 3, 5, 7, 10, 9, 8]), ('d', 'f', 'h', 'k', 'j', 'i')),
(tensor([1, 0]), ('b', 'a'))]
00:33:18 - Putting it all together
- The final product looks something like this:
for x, y, in dl:
pred = model(x)
loss = loss_func(pred, y)
loss.backward()
parameters -= parameters.grad * lr
weights = init_params((28*28), 1)
bias = init_params(1)
DataLoadercan be created fromDataset:
dl = DataLoader(dset, batch_size=256)
xb, yb = first(dl)
xb.shape, yb.shape
(torch.Size([256, 784]), torch.Size([256, 1]))
valid_dl = DataLoader(valid_dset, batch_size=256)
- Test model with a small batch
batch = train_x[:4]
batch.shape
torch.Size([4, 784])
valid_dl = DataLoader(valid_dset, batch_size=256)
- Create a mini-batch of size 4:
batch = train_x[:4]
batch.shape
torch.Size([4, 784])
preds = linear1(batch)
preds
tensor([ 2.4486, 0.5181, -1.5823, 5.9136], grad_fn=<AddBackward0>)
loss = mnist_loss(preds, train_y[:4])
loss
tensor(0.3213, grad_fn=<MeanBackward0>)
- Calculate the gradients:
loss.backward()
weights.grad.shape, weights.grad.mean(), bias.grad
(torch.Size([784]), tensor(-0.0161), tensor([-0.1128]))
- Can put into a function:
def calc_grad(xb, yb, model):
preds = model(xb)
loss = mnist_loss(preds, yb)
loss.backward()
- and test it:
calc_grad(batch, train_y[:4], linear1)
weights.grad.mean(), bias.grad
(tensor(-0.0323), tensor([-0.2256]))
00:37:02 - Zeroing gradients
- If you call it twice, you get a diff value each time, which shouldn't happen with the same gradients:
calc_grad(batch, train_y[:4], linear1)
weights.grad.mean(), bias.grad
(tensor(-0.0484), tensor([-0.3384]))
- That's because
backward()doesn't just calculate gradients. It adds them to stored gradients.- Set to zero first:
- Note
zero_sets to matrix of 0s inplace.
- Note
- Set to zero first:
weights.grad.zero_()
bias.grad.zero_()
tensor([0.])
00:38:05 - Train a single epoch
- Note the use of
.datato avoid calculating gradient during gradient descent step
def train_epoch(model, lr, params):
for xb, yb in dl:
calc_grad(xb, yb, model)
for p in params:
p.data -= p.grad * lr
p.grad.zero_()
00:39:31 - SGD vs Gradient Descent
Gradient descent doesn't have the loop part: it calculates all in one go (and is rarely used in practice):
def train_epoch(model, lr, params): params = calc_grad(inputs, labels, model) for p in params: p.data -= p.grad * lr p.grad.zero_()
00:40:15 - New accuracy metric
- Since we are applying the [[Sigmoid Function]] to the predictions, the 0 position now becomes 0.5, so we decide it's a 3 if greater than 0.5
def batch_accuracy(xb, yb):
preds = xb.sigmoid()
correct = (preds>0.5) == yb
return correct.float().mean()
linear1(batch).sigmoid() > 0.5
tensor([ True, True, False, True])
batch_accuracy(linear1(batch), train_y[:4])
tensor(0.7500)
- Now a function to do for all batches in the validation set.
def validate_epoch(model):
accs = [batch_accuracy(model(xb), yb) for xb, yb in valid_dl]
return round(torch.stack(accs).mean().item(), 4)
validate_epoch(linear1)
0.841
- Since the model hasn't been trained, a 50% accuracy is what we'd expect for picking values at random.
- Let's train for one epoch:
lr = 1.
params = weights, bias
train_epoch(linear1, lr, params)
validate_epoch(linear1)
0.6945
- And more:
for i in range(20):
train_epoch(linear1, lr, params)
print(validate_epoch(linear1), end=' ')
0.8309 0.8962 0.9277 0.9381 0.9433 0.9513 0.954 0.9574 0.9588 0.9608 0.9627 0.9641 0.9656 0.966 0.9689 0.9693 0.9708 0.9708 0.9713 0.9713
- Which is about the same accuracy as pixel similarity approach.
00:43:31 - Refactoring and optimisers
- We can simplify the linear model using the PyTorch class
nn.Linearwhich stores the weights abd bias in an object:
linear_model = nn.Linear(28*28, 1)
- You can access the parameters of the model as follows:
w, b = linear_model.parameters()
w.shape, b.shape
(torch.Size([1, 784]), torch.Size([1]))
- Can use that information to create an optimiser:
class BasicOptim:
def __init__(self, params, lr):
self.params = list(params)
self.lr = lr
def step(self, *args, **kwargs):
for p in self.params:
p.data -= p.grad.data * self.lr
def zero_grad(self, *args, **kwargs):
for p in self.params:
p.grad = None
- Can create optimiser by passing in model params:
opt = BasicOptim(linear_model.parameters(), lr)
- Now to simplify training loop:
def train_epoch(model):
for xb, yb in dl:
calc_grad(xb, yb, model)
opt.step()
opt.zero_grad()
def train_model(model, epochs):
for i in range(epochs):
train_epoch(model)
print(validate_epoch(model), end=' ')
train_model(linear_model, 20)
0.4932 0.811 0.8452 0.9155 0.9365 0.9507 0.959 0.9658 0.9692 0.9707 0.9731 0.9751 0.9766 0.978 0.979 0.98 0.9805 0.9805 0.981 0.9814
fastaiprovides theSGDclass which does the same thing asBasicOptim
linear_model = nn.Linear(28*28, 1)
opt = SGD(linear_model.parameters(), lr)
train_model(linear_model, 20)
0.4932 0.6548 0.8613 0.9175 0.9375 0.9526 0.9595 0.9668 0.9692 0.9702 0.9727 0.9746 0.9766 0.9775 0.979 0.98 0.98 0.981 0.9814 0.9819
00:48:50 - DataLoaders
- DataLoaders a tiny class that stores dataloaders.
- Does things like shuffling training dataloader, and not shuffling validation dataloader.
dls = DataLoaders(dl, valid_dl)
00:49:50 - Learning class
- Takes DataLoaders, model, optim function, loss function and metrics: does all the stuff we just did manually.
learn = Learner(
dls, nn.Linear(28*28, 1), opt_func=SGD,
loss_func=mnist_loss, metrics=batch_accuracy)
- Fit then performs train epochs:
learn.fit(10, lr=lr)
| epoch | train_loss | valid_loss | batch_accuracy | time |
|---|---|---|---|---|
| 0 | 0.636430 | 0.278792 | 0.500000 | 00:00 |
| 1 | 0.483960 | 0.419207 | 0.827723 | 00:00 |
| 2 | 0.179003 | 0.383422 | 0.845545 | 00:00 |
| 3 | 0.079260 | 0.310101 | 0.914356 | 00:00 |
| 4 | 0.042483 | 0.278336 | 0.934653 | 00:00 |
| 5 | 0.028095 | 0.259542 | 0.950000 | 00:00 |
| 6 | 0.022161 | 0.246771 | 0.958416 | 00:00 |
| 7 | 0.019506 | 0.237432 | 0.965347 | 00:00 |
| 8 | 0.018148 | 0.230164 | 0.968317 | 00:00 |
| 9 | 0.017325 | 0.224218 | 0.969802 | 00:00 |
00:51:09 - Adding non-linearity
- What we've done so far is created a linear function. That's not a neural network.
- A neural network requires a non-linearity to handle more complex task.
- One thing we can add is a function that replaces negative numbers with 0.
def simple_net(xb):
res = xb@w1 + b1
res = res.max(tensor(0.0))
res = res@w2 + b2
return res
w1andw2are weight tensors, andb1andb2are bias tensors; parameters initially randomly initialised like the previous section.- Since
w1has 30 output activations,w2must have 30 input activations.
- Since
w1 = init_params((28*28,30))
b1 = init_params(30)
w2 = init_params((30, 1))
b2 = init_params(1)
- The
res.max(tensor(0.0))is called a [[Rectified Linear Unit [[ReLU]].- Also available in PyTorch as
F.relu:
- Also available in PyTorch as
plot_function(F.relu)
- ReLU is an example of a a lot of things sound complicated and "scary" in Deep Learning, but are quite simple at their core.
00:53:24 - Why non-linear activation like RelU?
- Without a non-linear activation function, 2 linear layers could simply be rewritten as a single linear layer.
- With a non-linear, you should be able to approximate any arbitrary function according to Universal Approximation Theory.
00:55:03 - On Deep Learning's simplicity
- One of the challenges beginner's have: there isn't anything more to neural networks than the function above.
- Some tricks to use less parameters etc
- Most of it is just performance tweaks.
00:55:54 - Refactoring and functional composition
- [[Function Composition]] is when you take the result from one function and pass into the next.
- A [[Neural Network]] is just [[Function Composition]] of linear layers and activation functions.
- PyTorch provides a tool for functional composition called
nn.Sequential:
simple_net = nn.Sequential(
nn.Linear(28*28, 30),
nn.ReLU(),
nn.Linear(30, 1)
)
00:57:08 - Question: does ReLU cause many zero gradients?
- Yes, it does. However, they won't be zero for every image. And it definitely won't be for every minibatch.
- It is still a waste of computation having "inactive neurons".
- There is an approach that will show you how many dead neurons in the network that is shown later in the course.
- Leaky ReLU is one alternative approach:
res.max(0.0001)
- It is still a waste of computation having "inactive neurons".
00:59:27 - Training neural network
learn = Learner(dls, simple_net, opt_func=SGD, loss_func=mnist_loss, metrics=batch_accuracy)
- Note that as layers get deeper, you tend to have to have smaller learning rates as they tend to be "bumpier".
learn.fit(40, 0.1)
| epoch | train_loss | valid_loss | batch_accuracy | time |
|---|---|---|---|---|
| 0 | 0.280491 | 0.632562 | 0.500000 | 00:00 |
| 1 | 0.135367 | 0.440245 | 0.811881 | 00:00 |
| 2 | 0.076484 | 0.328983 | 0.920297 | 00:00 |
| 3 | 0.051259 | 0.290111 | 0.947030 | 00:00 |
| 4 | 0.039429 | 0.270758 | 0.964851 | 00:00 |
| 5 | 0.033302 | 0.258871 | 0.968812 | 00:00 |
| 6 | 0.029734 | 0.250580 | 0.970792 | 00:00 |
| 7 | 0.027389 | 0.244271 | 0.972772 | 00:00 |
| 8 | 0.025684 | 0.239149 | 0.973762 | 00:00 |
| 9 | 0.024353 | 0.234804 | 0.975248 | 00:00 |
| 10 | 0.023266 | 0.231001 | 0.976733 | 00:00 |
| 11 | 0.022354 | 0.227595 | 0.978218 | 00:00 |
| 12 | 0.021574 | 0.224493 | 0.979208 | 00:00 |
| 13 | 0.020898 | 0.221629 | 0.981188 | 00:00 |
| 14 | 0.020304 | 0.218957 | 0.981188 | 00:00 |
| 15 | 0.019777 | 0.216442 | 0.982178 | 00:00 |
| 16 | 0.019306 | 0.214061 | 0.982673 | 00:00 |
| 17 | 0.018881 | 0.211795 | 0.983168 | 00:00 |
| 18 | 0.018495 | 0.209629 | 0.983168 | 00:00 |
| 19 | 0.018143 | 0.207553 | 0.983168 | 00:00 |
| 20 | 0.017819 | 0.205562 | 0.983168 | 00:00 |
| 21 | 0.017519 | 0.203653 | 0.984158 | 00:00 |
| 22 | 0.017241 | 0.201821 | 0.984653 | 00:00 |
| 23 | 0.016981 | 0.200059 | 0.985148 | 00:00 |
| 24 | 0.016738 | 0.198360 | 0.985644 | 00:00 |
| 25 | 0.016510 | 0.196717 | 0.985644 | 00:00 |
| 26 | 0.016295 | 0.195123 | 0.986139 | 00:00 |
| 27 | 0.016092 | 0.193574 | 0.986139 | 00:00 |
| 28 | 0.015900 | 0.192065 | 0.986634 | 00:00 |
| 29 | 0.015718 | 0.190590 | 0.986634 | 00:00 |
| 30 | 0.015544 | 0.189148 | 0.987129 | 00:00 |
| 31 | 0.015379 | 0.187737 | 0.987129 | 00:00 |
| 32 | 0.015221 | 0.186357 | 0.987129 | 00:00 |
| 33 | 0.015070 | 0.185007 | 0.987129 | 00:00 |
| 34 | 0.014926 | 0.183688 | 0.987624 | 00:00 |
| 35 | 0.014788 | 0.182404 | 0.987624 | 00:00 |
| 36 | 0.014654 | 0.181153 | 0.987624 | 00:00 |
| 37 | 0.014526 | 0.179936 | 0.987624 | 00:00 |
| 38 | 0.014403 | 0.178753 | 0.987624 | 00:00 |
| 39 | 0.014284 | 0.177603 | 0.987624 | 00:00 |
- Can use
recorderto peer inside learner.valuescan be used to plot accuracy over training:
plt.plot(L(learn.recorder.values).itemgot(2));
and can get final accuracy:
learn.recorder.values[-1][2]
0.9876237511634827
- We now have a function that can solve any problem to any level of accuracy, if we can find the right parameters.
01:01:53 - How can we use the learner to visual the model?
- Can use
learn.modelto see the model:
m = learn.model
m
Sequential( (0): Linear(in_features=784, out_features=30, bias=True) (1): ReLU() (2): Linear(in_features=30, out_features=1, bias=True) )
w,b = m[0].parameters()
w.shape
torch.Size([30, 784])
w2, b2 = m[2].parameters()
w2.shape
torch.Size([1, 30])
show_image(w[0].view((28, 28)))
<AxesSubplot:>
01:05:30 - Train models with approach from Lesson 1
dls = ImageDataLoaders.from_folder(path)
learn = cnn_learner(dls, resnet18, pretrained=False,
loss_func=F.cross_entropy, metrics=accuracy)
learn.fit_one_cycle(1, 0.1)
| epoch | train_loss | valid_loss | accuracy | time |
|---|---|---|---|---|
| 0 | 0.115656 | 0.017766 | 0.996075 | 00:19 |
- Nearly 100% accuracy! Clearly the tricks help.
01:06:20 - Jargon recap
ReLU- function that convert negative numbers into 0.mini-batch- A couple of inputs and labels.forward pass- Applying the model to your inputs and calculting loss.loss- Value that tells you how model is doing.gradient- Derivate of loss with respect to parameters of the model.backward pass- Compute gradients of the loss with respect to model params.gradient descent- Take a step in directions opposite to gradients to make model params better.learning rate- Size of step we take when applying SGD to update params of model.parameters- numbers we're learningactivations- numbers we're calculatingTensor ranks:
- rank zero: scalar
- rank one: vector
- rank two: matrix
01:09:43 - Pet breeds image classification
- How do we understand the data in our data set?
- Can use ls to see inside dataset.
from fastai.vision.all import *
path = untar_data(URLs.PETS)
# For conveinence so that the base path is hidden when printing.
Path.BASE_PATH = path
path.ls()
(#2) [Path('images'),Path('annotations')]
- Can take a look at the images directory:
(path/"images").ls()
(#7393) [Path('images/beagle_115.jpg'),Path('images/boxer_18.jpg'),Path('images/Maine_Coon_157.jpg'),Path('images/scottish_terrier_28.jpg'),Path('images/english_setter_6.jpg'),Path('images/american_pit_bull_terrier_79.jpg'),Path('images/boxer_128.jpg'),Path('images/Persian_265.jpg'),Path('images/Maine_Coon_182.jpg'),Path('images/keeshond_89.jpg')...]
01:12:08 - L class revisited
- Methods in fastai that would return a list generally return an
Lclass. Lhas a number of enhanced:- When it prints, it tells you how many items in the list and truncates the output.
01:12:50 - Generating labels with regex
- Since we know that dogs labels first character is lower case, we can use that to filter.
- Will use regex for filtering. To start, let's build the regex on one example:
fname = (path/"images").ls()[0]
re.findall(r'(.+)_\d+.jpg$', fname.name)
['beagle']
- In Python,
rbefore a string says: don't treat\special. - Can use that to create a DataBlock of breeds:
pets = DataBlock(
blocks=(ImageBlock, CategoryBlock),
get_items=get_image_files,
splitter=RandomSplitter(seed=42),
get_y=using_attr(RegexLabeller(r'(.+)_\d+.jpg$'), 'name'),
item_tfms=Resize(460),
batch_tfms=aug_transforms(size=224, min_scale=0.75)
)
dls = pets.dataloaders(path/"images")
01:17:11 - Presizing
These 2 lines are performance a
fastaidata augmentation strategy called "presizing":item_tfms=Resize(460), batch_tfms=aug_transforms(size=224, min_scale=0.75)- Resize to a reasonable size with cropping, then perform resizing for each batch.
- 2 advantage to this:
- Because 1st step creates something square, 2nd step can happen on GPU.
- Normally doing zoom, rotate and warp is destructive to image and is slow.
- Fastai keeps track of coordinate transforms, then performs them all in one go.
01:21:48 - Are blocks in DataBlocks an ordered list
- Yes, it's an ordered list: independant and dependant variable.
01:22:51 - Checking and debugging a DataBlock
- Want to check DataBlocks are working correctly by visualing examples:
dls.show_batch(nrows=1, ncols=3)
- Can add
unique=Trueto show the same image with different augementations:
dls.show_batch(nrows=1, ncols=4, unique=True)
- Call
summaryon DataBlock to tell you everything that's happening:
pets.summary(path/"images")
Setting-up type transforms pipelines
Collecting items from /root/.fastai/data/oxford-iiit-pet/images
Found 7390 items
2 datasets of sizes 5912,1478
Setting up Pipeline: PILBase.create
Setting up Pipeline: partial -> Categorize -- {'vocab': None, 'sort': True, 'add_na': False}
Building one sample
Pipeline: PILBase.create
starting from
/root/.fastai/data/oxford-iiit-pet/images/basset_hound_43.jpg
applying PILBase.create gives
PILImage mode=RGB size=333x500
Pipeline: partial -> Categorize -- {'vocab': None, 'sort': True, 'add_na': False}
starting from
/root/.fastai/data/oxford-iiit-pet/images/basset_hound_43.jpg
applying partial gives
basset_hound
applying Categorize -- {'vocab': None, 'sort': True, 'add_na': False} gives
TensorCategory(14)
Final sample: (PILImage mode=RGB size=333x500, TensorCategory(14))
Collecting items from /root/.fastai/data/oxford-iiit-pet/images
Found 7390 items
2 datasets of sizes 5912,1478
Setting up Pipeline: PILBase.create
Setting up Pipeline: partial -> Categorize -- {'vocab': None, 'sort': True, 'add_na': False}
Setting up after_item: Pipeline: Resize -- {'size': (460, 460), 'method': 'crop', 'pad_mode': 'reflection', 'resamples': (2, 0), 'p': 1.0} -> ToTensor
Setting up before_batch: Pipeline:
Setting up after_batch: Pipeline: IntToFloatTensor -- {'div': 255.0, 'div_mask': 1} -> Flip -- {'size': None, 'mode': 'bilinear', 'pad_mode': 'reflection', 'mode_mask': 'nearest', 'align_corners': True, 'p': 0.5} -> RandomResizedCropGPU -- {'size': (224, 224), 'min_scale': 0.75, 'ratio': (1, 1), 'mode': 'bilinear', 'valid_scale': 1.0, 'max_scale': 1.0, 'p': 1.0} -> Brightness -- {'max_lighting': 0.2, 'p': 1.0, 'draw': None, 'batch': False}
Building one batch
Applying item_tfms to the first sample:
Pipeline: Resize -- {'size': (460, 460), 'method': 'crop', 'pad_mode': 'reflection', 'resamples': (2, 0), 'p': 1.0} -> ToTensor
starting from
(PILImage mode=RGB size=333x500, TensorCategory(14))
applying Resize -- {'size': (460, 460), 'method': 'crop', 'pad_mode': 'reflection', 'resamples': (2, 0), 'p': 1.0} gives
(PILImage mode=RGB size=460x460, TensorCategory(14))
applying ToTensor gives
(TensorImage of size 3x460x460, TensorCategory(14))
Adding the next 3 samples
No before_batch transform to apply
Collating items in a batch
Applying batch_tfms to the batch built
Pipeline: IntToFloatTensor -- {'div': 255.0, 'div_mask': 1} -> Flip -- {'size': None, 'mode': 'bilinear', 'pad_mode': 'reflection', 'mode_mask': 'nearest', 'align_corners': True, 'p': 0.5} -> RandomResizedCropGPU -- {'size': (224, 224), 'min_scale': 0.75, 'ratio': (1, 1), 'mode': 'bilinear', 'valid_scale': 1.0, 'max_scale': 1.0, 'p': 1.0} -> Brightness -- {'max_lighting': 0.2, 'p': 1.0, 'draw': None, 'batch': False}
starting from
(TensorImage of size 4x3x460x460, TensorCategory([14, 23, 18, 15], device='cuda:0'))
applying IntToFloatTensor -- {'div': 255.0, 'div_mask': 1} gives
(TensorImage of size 4x3x460x460, TensorCategory([14, 23, 18, 15], device='cuda:0'))
applying Flip -- {'size': None, 'mode': 'bilinear', 'pad_mode': 'reflection', 'mode_mask': 'nearest', 'align_corners': True, 'p': 0.5} gives
(TensorImage of size 4x3x460x460, TensorCategory([14, 23, 18, 15], device='cuda:0'))
applying RandomResizedCropGPU -- {'size': (224, 224), 'min_scale': 0.75, 'ratio': (1, 1), 'mode': 'bilinear', 'valid_scale': 1.0, 'max_scale': 1.0, 'p': 1.0} gives
(TensorImage of size 4x3x224x224, TensorCategory([14, 23, 18, 15], device='cuda:0'))
applying Brightness -- {'max_lighting': 0.2, 'p': 1.0, 'draw': None, 'batch': False} gives
(TensorImage of size 4x3x224x224, TensorCategory([14, 23, 18, 15], device='cuda:0'))
01:24:39 - How does presize work if image is smaller than resize value
- By default uses Crop. If image is smaller than resize value then it will stretch or zoom.
01:25:46 - Data cleaning?
- A lot of people say do data cleaning before training model.
- Fastai don't recommend that.
- Let the model show problems with your data.
- Once you have working datablock, then you can train model then use the ImageClassifierCleaner to clean data and retrain.
01:26:44 - fastai auto selecting loss function
- When we create a
cnn_learner, we don't have to pass a loss function as fastai picks one for you.- Can view the loss_func picked with
learn.loss_func
- Can view the loss_func picked with
learn = cnn_learner(dls, resnet34, metrics=error_rate)
Downloading: "https://download.pytorch.org/models/resnet34-333f7ec4.pth" to /root/.cache/torch/hub/checkpoints/resnet34-333f7ec4.pth
learn.loss_func
FlattenedLoss of CrossEntropyLoss()
learn.fine_tune(2)
| epoch | train_loss | valid_loss | error_rate | time |
|---|---|---|---|---|
| 0 | 1.511772 | 0.356324 | 0.110284 | 01:05 |
| epoch | train_loss | valid_loss | error_rate | time |
|---|---|---|---|---|
| 0 | 0.513008 | 0.295677 | 0.100135 | 01:05 |
| 1 | 0.331849 | 0.226082 | 0.074425 | 01:05 |
learn.save('pet_breeds_l4')
Path('models/pet_breeds_l4.pth')
01:27:38 - CrossEntropyLoss
- Cross entropy loss is similar to
mnist_lossfrom earlier, but has 2 benefits:- The
torch.whereonly works when you have a [[Binary outcome]]. - CrossEntropyLoss works when you have more than 2 categories.
- The
01:28:48 - Why build a model before building the model?
- We train a model then clean, then retrain on a cleaner dataset.
01:29:52 - CrossEntropyLoss (cont)
- To understand CrossEntropyLoss we can first grab a single batch of data:
x, y = dls.one_batch()
- The dependant variables is an integer from 0 - 36 which can be indexed into vocab:
dls.vocab
['Abyssinian', 'Bengal', 'Birman', 'Bombay', 'British_Shorthair', 'Egyptian_Mau', 'Maine_Coon', 'Persian', 'Ragdoll', 'Russian_Blue', 'Siamese', 'Sphynx', 'american_bulldog', 'american_pit_bull_terrier', 'basset_hound', 'beagle', 'boxer', 'chihuahua', 'english_cocker_spaniel', 'english_setter', 'german_shorthaired', 'great_pyrenees', 'havanese', 'japanese_chin', 'keeshond', 'leonberger', 'miniature_pinscher', 'newfoundland', 'pomeranian', 'pug', 'saint_bernard', 'samoyed', 'scottish_terrier', 'shiba_inu', 'staffordshire_bull_terrier', 'wheaten_terrier', 'yorkshire_terrier']
y
TensorCategory([ 6, 29, 12, 30, 14, 6, 19, 7, 30, 8, 9, 2, 19, 1, 31, 29, 23, 19,
5, 17, 28, 8, 11, 30, 14, 26, 10, 18, 32, 15, 17, 8, 7, 34, 10, 32,
8, 27, 23, 11, 12, 8, 27, 15, 4, 34, 1, 16, 11, 8, 31, 11, 36, 16,
12, 19, 20, 34, 17, 34, 11, 36, 17, 1], device='cuda:0')
- Now can view the predictions:
preds, _ = learn.get_preds(dl=[(x,y)])
preds[0]
tensor([3.6852e-07, 5.2262e-07, 1.8340e-07, 1.1055e-07, 1.4786e-04, 9.6826e-07,
9.9906e-01, 7.4665e-04, 2.6897e-05, 5.1130e-07, 1.7674e-09, 1.1774e-07,
2.0554e-07, 4.7894e-08, 2.7261e-08, 1.0484e-07, 2.6598e-08, 1.7616e-08,
1.1701e-08, 3.4770e-07, 1.4825e-07, 1.6700e-07, 2.3472e-07, 1.2853e-06,
4.8577e-06, 1.2756e-06, 2.0426e-08, 1.1547e-07, 3.8929e-06, 4.3687e-08,
2.5229e-07, 1.1356e-06, 2.2247e-08, 6.1095e-08, 6.3751e-09, 1.9749e-08,
3.2576e-07])
- Can see the preds add up to one:
preds[0].sum()
tensor(1.0000)
01:32:38 - Softmax
- To make sets of predictions like this that are probability-like, we use Softmax.
- Softmax is "basically an extension of Sigmoid"
- What if we wanted more categories than just 2?
torch.random.manual_seed(42);
- Creating random numbers with standard deviation of 2 to resemble a model with 2 activations (is 3 or is 7)
acts = torch.randn((6, 2)) * 2
acts
tensor([[ 0.6734, 0.2576],
[ 0.4689, 0.4607],
[-2.2457, -0.3727],
[ 4.4164, -1.2760],
[ 0.9233, 0.5347],
[ 1.0698, 1.6187]])
- If we simply apply Sigmoid, we don't get something that's probability like ie sums to 1.
acts.sigmoid()
tensor([[0.6623, 0.5641],
[0.6151, 0.6132],
[0.0957, 0.4079],
[0.9881, 0.2182],
[0.7157, 0.6306],
[0.7446, 0.8346]])
- What we care about is the relative confidence of being a "3 vs a "7".
- So we can calculate the relative difference between activations and take the sigmoid of that:
diff = acts[:,0]-acts[:,1]
diff
tensor([ 0.4158, 0.0083, -1.8731, 5.6924, 0.3886, -0.5489])
diff.sigmoid()
tensor([0.6025, 0.5021, 0.1332, 0.9966, 0.5959, 0.3661])
- To get 2 columns, we could subtract 1-value from the second column (prob of being 7):
torch.stack([diff.sigmoid(), 1-diff.sigmoid()], dim=1)
tensor([[0.6025, 0.3975],
[0.5021, 0.4979],
[0.1332, 0.8668],
[0.9966, 0.0034],
[0.5959, 0.4041],
[0.3661, 0.6339]])
- How do we do this for more than 2 cols?
- We can use the [[Softmax Function]]
def softmax(x):
return exp(x) / exp(x).sum(dim=1, keepdim=True)
- Softmax is: $$\frac{e^x}{\sum\limits_{i=1}^{N}e^{x_i}}$$
- The Exponential function (exp) is defined as:
e**x.eis a special number approximately equal to 2.718. It increases rapidly.
- The Exponential function (exp) is defined as:
- Can see
softmaxreturns same value assigmoidfor the first column:
sm_acts = torch.softmax(acts, dim=1)
sm_acts
tensor([[0.6025, 0.3975],
[0.5021, 0.4979],
[0.1332, 0.8668],
[0.9966, 0.0034],
[0.5959, 0.4041],
[0.3661, 0.6339]])
- In the multi-category case, softmax would look something like:
outputs = tensor([0.02, -2.49, 1.25])
outputs
tensor([ 0.0200, -2.4900, 1.2500])
exp = outputs.exp()
exp
tensor([1.0202, 0.0829, 3.4903])
softmax = torch.softmax(outputs, dim=0)
softmax
tensor([0.2221, 0.0180, 0.7599])
- Softmax tends to "amplify" a single result.
- Which may not be what you always want.
- An alternative is multiple binary values with Sigmoid.
01:40:38 - Log likelihood
- We could take a similar approach to the
mnist_lossfunction by replacingtorch.wherewith indexing:
def mnist_loss(predictions, targets):
return torch.where(targets==1, 1-predictions, predictions).mean()
targ = tensor([0,1,0,1,1,0])
sm_acts
tensor([[0.6025, 0.3975],
[0.5021, 0.4979],
[0.1332, 0.8668],
[0.9966, 0.0034],
[0.5959, 0.4041],
[0.3661, 0.6339]])
- then for each of the
targwe can use that column ofsm_actsusing tensor indexing.- Here we are requesting rows between 0 and 6 and columns that match the targs:
idx = range(6)
sm_acts[idx, targ]
tensor([0.6025, 0.4979, 0.1332, 0.0034, 0.4041, 0.3661])
df = pd.DataFrame(sm_acts, columns=['3', '7'])
df['targ'] = targ
df['idx'] = idx
df['loss'] = sm_acts[range(6), targ]
t = df.style.hide_index()
df
| 3 | 7 | targ | idx | loss | |
|---|---|---|---|---|---|
| 0 | 0.602469 | 0.397531 | 0 | 0 | 0.602469 |
| 1 | 0.502065 | 0.497935 | 1 | 1 | 0.497935 |
| 2 | 0.133188 | 0.866811 | 0 | 2 | 0.133188 |
| 3 | 0.996640 | 0.003360 | 1 | 3 | 0.003360 |
| 4 | 0.595949 | 0.404051 | 1 | 4 | 0.404051 |
| 5 | 0.366118 | 0.633882 | 0 | 5 | 0.366118 |
- This works with more than 2 columns
- PyTorch provides a function that does that same thing as
sm_acts[range(n), targ]- Note: it takes a negative, because the log taken after gives us negative numbers, called
nll_loss(NLL stands for negative log likelihood):
- Note: it takes a negative, because the log taken after gives us negative numbers, called
-sm_acts[idx, targ]
tensor([-0.6025, -0.4979, -0.1332, -0.0034, -0.4041, -0.3661])
F.nll_loss(sm_acts, targ, reduction='none')
tensor([-0.6025, -0.4979, -0.1332, -0.0034, -0.4041, -0.3661])
- Confusingly, negative log likelihood doesn't take the log.
01:44:31 - Logs
- It works as a loss function, but can be better.
- Using probabilities, it cannot be smaller than zero, or greater than one
- Model will not care it it's predicting 0.99 or 0.999
- But the error if there's 1000 things, the first means 10 thing is wrong, the 2nd means 1 thing are wrong.
- Want to translate predictions as being between -infinity and infinity.
- There is a function in Math that does it: the logarithm:
plot_function(torch.log, min=0, max=1)
torch.log(tensor(0.)), torch.log(tensor(0.2)), torch.log(tensor(0.99)), torch.log(tensor(0.999))
(tensor(-inf), tensor(-1.6094), tensor(-0.0101), tensor(-0.0010))
- The logarithm has this identity:
y = b**a
a = log(y, b)
In this case,
log(y, b)returnslog y base b.- PyTorch doesn't define
logthis way. It uses special number $e$ (2.718...) as the base
- PyTorch doesn't define
This relationship is one of the most important things about logarithms:
log(a*b) = log(a) + log(b)
- Because
a*bcan get big or small, that the precision in computers floating point math can get out of hand.- But, adding doesn't.
01:47:54 - Cross-Entropy Loss
- If you take the softmax, then take the log, then pass to
nll_loss, it's calledcross entropy loss
loss_func = nn.CrossEntropyLoss()
loss_func(acts, targ)
tensor(1.8045)
- In PyTorch, you can usually use
nn.Classversion orF.func_name. Usually the class version if preferred:
F.cross_entropy(acts, targ)
tensor(1.8045)
- Errata: at 01:49:56: says "camel case" name where he means "snake case".
- It returns a single number as that's required for the loss function, but the individual values are available passing
reduction=none:
nn.CrossEntropyLoss(reduction='none')(acts, targ)
tensor([0.5067, 0.6973, 2.0160, 5.6958, 0.9062, 1.0048])
01:51:46 - Data ethics and mask efficiency
- Jeremy pointed at that masks are likely very effective.
- Found himself as face of global advocacy campaign.
- Found data around masks was misunderstood.
- Role of data scientist: to understand data and do something about it.
- What was clear: people weren't convinced by science, especially when WHO and CDC are saying the opposite.
- Put together a team to review the evidence and wrote this paper.
- Knowing the data isn't effective unless you can communicate to decision makers.