fastai - Lesson 6 - Deep Learning for Coders (2020)
Notes taken from the Deep Learning for Coders (2020) - Lesson 6 video
Lex Toumbourou ·
Notes taken from watching the Lesson 6 - Deep Learning for Coders (2020) video.
Had to install latest version to work around this issue: https://forums.fast.ai/t/poor-results-when-running-chapter-5-examples/87161
In [1]:
!pip install git+https://github.com/fastai/fastai/ &>/dev/null
In [2]:
from fastai.vision.all import *
In [3]:
import fastai
print(fastai.__version__)
2.4.2
In [4]:
path = untar_data(URLs.PETS)
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")
/opt/conda/lib/python3.7/site-packages/torch/_tensor.py:1023: UserWarning: torch.solve is deprecated in favor of torch.linalg.solveand will be removed in a future PyTorch release. torch.linalg.solve has its arguments reversed and does not return the LU factorization. To get the LU factorization see torch.lu, which can be used with torch.lu_solve or torch.lu_unpack. X = torch.solve(B, A).solution should be replaced with X = torch.linalg.solve(A, B) (Triggered internally at /pytorch/aten/src/ATen/native/BatchLinearAlgebra.cpp:760.) ret = func(*args, **kwargs)
In [5]:
learn = cnn_learner(dls, resnet34, metrics=error_rate)
Downloading: "https://download.pytorch.org/models/resnet34-b627a593.pth" to /root/.cache/torch/hub/checkpoints/resnet34-b627a593.pth
/opt/conda/lib/python3.7/site-packages/torch/nn/functional.py:718: UserWarning: Named tensors and all their associated APIs are an experimental feature and subject to change. Please do not use them for anything important until they are released as stable. (Triggered internally at /pytorch/c10/core/TensorImpl.h:1156.) return torch.max_pool2d(input, kernel_size, stride, padding, dilation, ceil_mode)
In [6]:
learner = learn.load('/kaggle/input/lesson-4/models/pet_breeds_l4')
00:00:00 - Lesson 4 review
- Ensure you understand Cross Entropy loss before continuing on.
00:01:09 - Model interpretation
- A confusion matrix: displays the correct predictions in the diagonal.
In [7]:
interp = ClassificationInterpretation.from_learner(learn)
interp.plot_confusion_matrix(figsize=(12,12), dpi=60)
- When you have a lot of classes, it can be better to use
most_confused- Tells you combinations that it had most trouble with.
In [8]:
interp.most_confused(min_val=5)
Out[8]:
[('Ragdoll', 'Birman', 6),
('Siamese', 'Birman', 5),
('american_pit_bull_terrier', 'staffordshire_bull_terrier', 5)]
- Even experts don't agree about Birman and Ragdoll
- When model makes sensible mistakes, it's a good sign.
00:03:09 - Improving learning rate to train a better model
- One way to make model better is to improve learning rate.
- Bigger learning rate = faster training.
- However, too might and the model will overstep the minimum.
00:05:00 - Learning Rate Finder
- [[Learning Rate Finder]]
- Idea by Leslie Smith:
- Start with tiny learning rate.
- Take a minibatch and step, then record the losses.
- Increase learning rate by some %.
- Step again.
- Keep doing until loss gets worse.
- Select learning rate a bit before you go to far.
- Pick either:
- One order of magnitude less (the min divided by 10).
- Loss point where loss was decreasing.
- Idea by Leslie Smith:
lr_find()in fastai computes that for you.
In [9]:
learn = cnn_learner(dls, resnet34, metrics=error_rate)
learn.lr_find()
/opt/conda/lib/python3.7/site-packages/fastai/callback/schedule.py:270: UserWarning: color is redundantly defined by the 'color' keyword argument and the fmt string "ro" (-> color='r'). The keyword argument will take precedence. ax.plot(val, idx, 'ro', label=nm, c=color)
Out[9]:
SuggestedLRs(valley=0.0010000000474974513)
- You can then retrain using recommended epoch.
In [10]:
learn.fine_tune(2, base_lr=1e-3)
| epoch | train_loss | valid_loss | error_rate | time |
|---|---|---|---|---|
| 0 | 2.063722 | 0.389853 | 0.120433 | 01:07 |
| epoch | train_loss | valid_loss | error_rate | time |
|---|---|---|---|---|
| 0 | 0.577278 | 0.273993 | 0.082544 | 01:09 |
| 1 | 0.376261 | 0.234854 | 0.077131 | 01:09 |
- Learning rate finder was only discovered in 2015.
- Shows how many more discoveries in DL.
- People in engineering tend to prefer this completely automated.
00:09:34 - Question: Is the learning rate plotter plotted against a single mini-batch?
- It's plotted while walking through the DataLoader, as you normally would when training.
00:10:13 - Question: Is network reset after each mini-batch, or does it accumulate?
- It's not reset. We want to see it improving.
- At end of training, we go back to previous rates.
00:10:53 - Question: why would a single learning rate found at the start of training, still be useful after many epochs?
- Next section.
00:11:17 - Question: for the learning rate finder, why use the steepest and not minimum?
- The minimum is the point where it's no longer learning.
- We want somewhere before:
- Sylvan's favourite approach: minimum / 10.
00:12:09 - Transfer learning recap
- Firstly, neural network is a bunch of linear layers with activation functions between them.
- Each linear layers have parameters.
- After you train neural network on image net, have parameters that aren't random.
- Early layers learn about general ideas and later are more specfic.
- Transfer learning:
- Take parameters that have been trained.
- Get rid of very last layer and replace with random weights.
- Freeze the other layers, and just train the last layer.
00:13:50 - Learning rate finder simplicity
- Seems surprising that the learner rate finder can be so useful even though so simple and approximate.
- Wasn't really know until fastai started talking about it.
00:15:37 - Transer learning continued
fine_tuneis the method fastai uses to do it.
In [11]:
learn.fine_tune??
- Freeze everything up to the last layer:
self.freeze() - Then just fit the randomly added weights.
- Unfreeze all the other layers.
- Fit for some number of epochs.
00:18:13 - Fine tune by "hand"
- We don't have to use
fine_tune:
In [12]:
dls.show_batch()
In [13]:
learn = cnn_learner(dls, resnet34, metrics=error_rate)
In [14]:
learn.lr_find()
Out[14]:
SuggestedLRs(valley=0.0014454397605732083)
In [15]:
learn.fit_one_cycle(3, 3e-3)
| epoch | train_loss | valid_loss | error_rate | time |
|---|---|---|---|---|
| 0 | 1.122182 | 0.327917 | 0.113667 | 01:08 |
| 1 | 0.540056 | 0.241719 | 0.086604 | 01:06 |
| 2 | 0.338942 | 0.215674 | 0.073748 | 01:07 |
cnn_learneralready freezes the model for you.
- Then we can manually unfreeze the model:
In [16]:
learn.unfreeze()
00:18:45 - running lr_find after unfreezing
- You would assume it would be the wrong learning rate, so we can run lr_find again.
In [17]:
learn.lr_find()
Out[17]:
SuggestedLRs(valley=9.120108734350652e-05)
- The shape here is different because the model is trained.
- We generally take the point where it starts to get worse. Like 10-4 and divide by 10 back from that.
In [18]:
learn.fit_one_cycle(6, lr_max=1e-5)
| epoch | train_loss | valid_loss | error_rate | time |
|---|---|---|---|---|
| 0 | 0.251161 | 0.205250 | 0.070365 | 01:09 |
| 1 | 0.251269 | 0.201598 | 0.068336 | 01:09 |
| 2 | 0.218967 | 0.196250 | 0.066982 | 01:09 |
| 3 | 0.194220 | 0.191418 | 0.060893 | 01:09 |
| 4 | 0.181446 | 0.192967 | 0.062246 | 01:09 |
| 5 | 0.182578 | 0.191706 | 0.064953 | 01:09 |
00:19:32 - Discriminative learning rates
- Since we know later layers are more specific to the problem, and the earlier general.
- Want bigger learning rate for the later layers and smaller for earlier.
- Originally developed in ULMFiT
- Based on paper by Jason Yosinski who showed that layers should train at different speeds (though he didn't actually run the experiment)
- Fastai lets you pass
sliceobject- Slice is an object
- Very first layer will be 1e-6 and the last will be 1e-4
- Learning rates in between will be equally sliced.
In [19]:
learn = cnn_learner(dls, resnet34, metrics=error_rate)
learn.fit_one_cycle(3, 3e-3)
learn.unfreeze()
learn.fit_one_cycle(12, lr_max=slice(1e-6, 1e-4))
| epoch | train_loss | valid_loss | error_rate | time |
|---|---|---|---|---|
| 0 | 1.148988 | 0.391744 | 0.122463 | 01:08 |
| 1 | 0.516430 | 0.264915 | 0.085250 | 01:07 |
| 2 | 0.336015 | 0.221417 | 0.064953 | 01:07 |
| epoch | train_loss | valid_loss | error_rate | time |
|---|---|---|---|---|
| 0 | 0.264971 | 0.219408 | 0.069012 | 01:09 |
| 1 | 0.246522 | 0.208800 | 0.063599 | 01:10 |
| 2 | 0.228430 | 0.203198 | 0.064276 | 01:09 |
| 3 | 0.200924 | 0.199840 | 0.060893 | 01:09 |
| 4 | 0.167146 | 0.203511 | 0.062246 | 01:08 |
| 5 | 0.160588 | 0.194302 | 0.058863 | 01:09 |
| 6 | 0.157753 | 0.203722 | 0.062923 | 01:08 |
| 7 | 0.146432 | 0.198053 | 0.062246 | 01:08 |
| 8 | 0.128020 | 0.198028 | 0.060217 | 01:08 |
| 9 | 0.122255 | 0.197612 | 0.058187 | 01:09 |
| 10 | 0.127746 | 0.194809 | 0.054127 | 01:08 |
| 11 | 0.133889 | 0.195545 | 0.056834 | 01:09 |
00:22:06 - fit_one_cycle
- Starts at a low learning rate
- increases it gradually for first 1/3 until it gets to
lr_max - Then remaining 2/3, it gradually decreases.
- Developed by Leslie Smith (same person who did learning rate finder).
- Ignored by academic community.
- Looking at results above, we don't necessarily just want to take model with lowest weights.
- We'd like to train model again with number of epochs equal to the best error rate.
- Change epochs 12 to 8 and train from scratch.
00:22:56 - Plot loss
In [20]:
learn.recorder.plot_loss()
- Often see validation loss get worse before error rate gets worse.
00:24:40 - Improving model with deeper architectures and fp16
- Could try creating a deeper architecture: more pairs of linear layers and activation functions.
- For pretrained models, there's usually a small section of layer numbers available:
resnet18,resnet34,resnet50etc.
- For pretrained models, there's usually a small section of layer numbers available:
- Bigger models have more parameters and more gradients.
- It's common to run out of GPU memory:
Cuda runtime error: out of memory.- If it happens, restart notebook and try again with smaller learning rate.
- It's common to run out of GPU memory:
- Can add
fp_16()to use less precise numbers (half precision floating point, also called fp16).
In [21]:
from fastai.callback.fp16 import *
learn = cnn_learner(dls, resnet50, metrics=error_rate).to_fp16()
learn.fit_one_cycle(3, 3e-3)
learn.unfreeze()
learn.fit_one_cycle(8, lr_max=slice(1e-6, 1e-4))
Downloading: "https://download.pytorch.org/models/resnet50-0676ba61.pth" to /root/.cache/torch/hub/checkpoints/resnet50-0676ba61.pth
| epoch | train_loss | valid_loss | error_rate | time |
|---|---|---|---|---|
| 0 | 0.803505 | 0.371108 | 0.117050 | 01:12 |
| 1 | 0.433064 | 0.254045 | 0.069689 | 01:11 |
| 2 | 0.244331 | 0.209135 | 0.063599 | 01:11 |
| epoch | train_loss | valid_loss | error_rate | time |
|---|---|---|---|---|
| 0 | 0.178246 | 0.194852 | 0.060217 | 01:14 |
| 1 | 0.173872 | 0.186954 | 0.055480 | 01:14 |
| 2 | 0.146353 | 0.177561 | 0.052774 | 01:14 |
| 3 | 0.134664 | 0.181752 | 0.057510 | 01:15 |
| 4 | 0.104747 | 0.177655 | 0.060217 | 01:14 |
| 5 | 0.104844 | 0.176375 | 0.057510 | 01:14 |
| 6 | 0.102450 | 0.172244 | 0.054804 | 01:15 |
| 7 | 0.085970 | 0.173649 | 0.056834 | 01:14 |
- Trick is to use small models for as long as possible, to clean data and get pipeline ready etc.
00:28:36 - Question: how do you know or suspect that you can do better?
- Always assume you can do better, because you never know.
- Ask yourself: do you need to do better? Will model be useful enough now?
- Do something practical with model as soon as possible.
00:29:41 - Questionaire and further research
- Read paper by Leslie Smith that introduced the learning rate finder.
- Try to improve accuracy in chapter.
00:30:48 - Multi-label classification
Continued in next notebook.
In [1]:
!pip install git+https://github.com/fastai/fastai.git &> /dev/null
00:30:48 - Multi-label classification
- More than one or more labels per image.
00:32:01 - Question: is dropping floating point number precision have impact on final loss?
- Sometimes it makes it better.
- Doing some random, that creates more uncertainty.
- Usually doesn't make the difference either way.
00:33:53 - Exploring Pascal dataset.
- Pascal is a famous dataset with more than one kind of classified object per image.
In [2]:
from fastai.vision.all import *
path = untar_data(URLs.PASCAL_2007)
- Comes with a CSV which includes the labels for each image.
In [3]:
df = pd.read_csv(path/'train.csv')
df.head()
Out[3]:
| fname | labels | is_valid | |
|---|---|---|---|
| 0 | 000005.jpg | chair | True |
| 1 | 000007.jpg | car | True |
| 2 | 000009.jpg | horse person | True |
| 3 | 000012.jpg | car | False |
| 4 | 000016.jpg | bicycle | True |
- List of categories in each image, a space delimited string (eg
horse person)
00:34:30 - Pandas
- The pd above stands for Pandas: important library for data processing.
- Main class is
DataFrame, which is table of rows and columns. - Can access rows and columns with
ilocproperty. - Getting column 0:
In [4]:
df.iloc[:,0]
Out[4]:
0 000005.jpg
1 000007.jpg
2 000009.jpg
3 000012.jpg
4 000016.jpg
...
5006 009954.jpg
5007 009955.jpg
5008 009958.jpg
5009 009959.jpg
5010 009961.jpg
Name: fname, Length: 5011, dtype: object
- Getting row 0:
In [5]:
df.iloc[0,:]
Out[5]:
fname 000005.jpg labels chair is_valid True Name: 0, dtype: object
- Can also get column by name:
In [6]:
df['fname']
Out[6]:
0 000005.jpg
1 000007.jpg
2 000009.jpg
3 000012.jpg
4 000016.jpg
...
5006 009954.jpg
5007 009955.jpg
5008 009958.jpg
5009 009959.jpg
5010 009961.jpg
Name: fname, Length: 5011, dtype: object
- Can create a DataFrame from a dict:
In [7]:
tmp_df = pd.DataFrame({'a': [1, 2], 'b': [3,4]})
tmp_df
Out[7]:
| a | b | |
|---|---|---|
| 0 | 1 | 3 |
| 1 | 2 | 4 |
- Then create a new column by adding other columns:
In [8]:
tmp_df['c'] = tmp_df['a'] + tmp_df['b']
tmp_df
Out[8]:
| a | b | c | |
|---|---|---|---|
| 0 | 1 | 3 | 4 |
| 1 | 2 | 4 | 6 |
- Book recommendation: Python for Data Analysis by Wes Mckinney.
00:37:10 - Review Dataset and DataLoader
- We want to create DataBlock that we can train from.
- Reminder about
DatasetandDataLoaderDataset- anything you can index into:
In [9]:
a = list(enumerate(string.ascii_lowercase))
a[0], len(a)
Out[9]:
((0, 'a'), 26)
DataLoader- an interator that gives you a stream of mini batches with independent and dependent vars
In [10]:
dl_a = DataLoader(a, batch_size=8, shuffle=True)
b = first(dl_a)
b
Out[10]:
(tensor([18, 8, 25, 23, 19, 16, 3, 6]),
('s', 'i', 'z', 'x', 't', 'q', 'd', 'g'))
In [11]:
list(zip(b[0], b[1]))
Out[11]:
[(tensor(18), 's'), (tensor(8), 'i'), (tensor(25), 'z'), (tensor(23), 'x'), (tensor(19), 't'), (tensor(16), 'q'), (tensor(3), 'd'), (tensor(6), 'g')]
- Can use
*which inserts each element of list as arg in function.
In [12]:
list(zip(*b))
Out[12]:
[(tensor(18), 's'), (tensor(8), 'i'), (tensor(25), 'z'), (tensor(23), 'x'), (tensor(19), 't'), (tensor(16), 'q'), (tensor(3), 'd'), (tensor(6), 'g')]
00:40:04 - Datasets and DataLoaders
Datasetsis an object that has a training and validation dataset.Sometimes easiest to start by creating and testing
Datasets:
In [13]:
a = list(string.ascii_lowercase)
a[0], len(a)
Out[13]:
('a', 26)
In [14]:
dss = Datasets(a)
dss[0]
Out[14]:
('a',)
- Can use functions to create independent and dependent variables:
- (for example, open image and create lable from filename).
In [15]:
def f1(o): return o+'a'
def f2(o): return o+'b'
In [16]:
dss = Datasets(a, [[f1]])
dss[0]
Out[16]:
('aa',)
In [17]:
dss = Datasets(a, [[f1, f2]])
dss[0]
Out[17]:
('aab',)
In [18]:
dss = Datasets(a, [[f1], [f2]])
dss[0]
Out[18]:
('aa', 'ab')
- Can then create a
DataLoadersinstance:
In [19]:
dls = DataLoaders.from_dsets(dss, batch_size=4)
In [20]:
first(dls.train)
Out[20]:
(('ia', 'da', 'ca', 'ja'), ('ib', 'db', 'cb', 'jb'))
00:43:23 - DataBlock
- Instead of creating
DataLoadersfrom scratch, can use aDataBlock:
In [21]:
dblock = DataBlock()
In [22]:
dsets = dblock.datasets(df)
In [23]:
len(dsets.train), len(dsets.valid)
Out[23]:
(4009, 1002)
In [24]:
x, y = dsets.train[0]
x, y
Out[24]:
(fname 006958.jpg labels dog person diningtable is_valid False Name: 3512, dtype: object, fname 006958.jpg labels dog person diningtable is_valid False Name: 3512, dtype: object)
- It's returning the same thing in for both variables, as that's the deault behaviour.
- You can pass
get_xandget_yfunctions:
In [25]:
x['fname']
Out[25]:
'006958.jpg'
In [26]:
dblock = DataBlock(
get_x=lambda r: r['fname'],
get_y=lambda r: r['labels']
)
dsets = dblock.datasets(df)
dsets.train[0]
Out[26]:
('005888.jpg', 'dog')
- To save (serialize)
DataBlock, you can't use lambdas. - Next, we can add the full path to the image, and split on the label name:
In [27]:
Path.BASE_PATH = path
In [28]:
def get_x(r): return path/'train'/r['fname']
def get_y(r): return r['labels'].split(' ')
dblock = DataBlock(get_x=get_x, get_y=get_y)
dsets = dblock.datasets(df)
dsets.train[0]
Out[28]:
(Path('train/001680.jpg'), ['dog'])
- Since we want the image opened as an image,
blocksargument lets you say what sort of "block" you want for each item in tuple. - In the example below, we use
MultiCategoryBlockwhich expects a list of strings:
In [29]:
dblock = DataBlock(
blocks=(ImageBlock, MultiCategoryBlock),
get_x=get_x, get_y=get_y)
dsets = dblock.datasets(df)
dsets.train[0]
Out[29]:
(PILImage mode=RGB size=375x500,
TensorMultiCategory([0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 1., 0., 0., 1., 0., 0., 0.,
0., 0.]))
00:48:36 - One-Hot Encoding
- The labels there are [[One-Hot Encoded]]
- Each position corresponds to a possible label.
- If the label isn't in the image, it's 0. If it is 1.
- We can take a look at the first label, and see what it is:
In [30]:
idxs = torch.where(dsets.train[0][1]==1.)[0]
idxs, dsets.train.vocab[idxs]
Out[30]:
(TensorMultiCategory([ 4, 11, 14]), (#3) ['bottle','dog','person'])
00:50:44 - Question: shouldn't tensor be of integers?
- Conceptually it's a tensor of integers, but for the purpose of floating point calculations in cross entropy, we just make it a float.
00:51:30 - splitter function
- The
DataBlockuses a random split by default. - This dataset tells you what's in the validation set: usually a good idea to use the dataset's validation, so you can compare results.
- The
splitterfunction can be used to return the idxes in the train and validation set:
In [31]:
def splitter(df):
train = df.index[~df['is_valid']].tolist()
valid = df.index[df['is_valid']].tolist()
return train, valid
dblck = DataBlock(
blocks=(ImageBlock, MultiCategoryBlock),
splitter=splitter,
get_x=get_x,
get_y=get_y)
dsets = dblock.datasets(df)
dsets.train[0]
Out[31]:
(PILImage mode=RGB size=500x375,
TensorMultiCategory([0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0., 1.]))
00:52:39 - Question: why don't we use 8-bit precision?
- It's too flat and bumpy - hard to get good gradients.
- Sometimes they do a thing called "Quantization" to allow a model to do inference much faster.
00:53:54 - Finish DataBlock
- Now we add
item_tfmsto allow collation into DataLoader:
In [32]:
def splitter(df):
train = df.index[~df['is_valid']].tolist()
valid = df.index[df['is_valid']].tolist()
return train, valid
dblock = DataBlock(
blocks=(ImageBlock, MultiCategoryBlock),
splitter=splitter,
get_x=get_x,
get_y=get_y,
item_tfms=RandomResizedCrop(128, min_scale=0.35))
- Then
dataloadersto get a DataLoader back:
In [33]:
dls = dblock.dataloaders(df)
- And summary to ensure it all worked:
In [34]:
dls.show_batch(nrows=1, ncols=3)
00:54:48 - Binary cross entropy
- To start', we'll grab a learner using the dls above:
In [35]:
learn = cnn_learner(dls, resnet18)
Downloading: "https://download.pytorch.org/models/resnet18-f37072fd.pth" to /root/.cache/torch/hub/checkpoints/resnet18-f37072fd.pth
/opt/conda/lib/python3.7/site-packages/torch/nn/functional.py:718: UserWarning: Named tensors and all their associated APIs are an experimental feature and subject to change. Please do not use them for anything important until they are released as stable. (Triggered internally at /pytorch/c10/core/TensorImpl.h:1156.) return torch.max_pool2d(input, kernel_size, stride, padding, dilation, ceil_mode)
- Get one batch of predictions:
In [36]:
x, y = dls.train.one_batch()
In [37]:
activs = learn.model.cuda()(x)
- Examine one set of activations for an item in the batch:
In [38]:
activs[0]
Out[38]:
tensor([ 4.0968, -1.0870, 0.5517, -0.9677, 0.7336, -0.6855, -2.5957, -0.5128,
1.8058, -0.1456, 2.2515, -2.8793, -0.2660, -0.4388, 1.7154, -2.8521,
0.2223, -1.6583, -0.3429, 1.3525], device='cuda:0',
grad_fn=<SelectBackward>)
00:56:10 - Question: Is DataBlock API compatible with libraries like Dask?
- DataBlock API allows you to pass anything that's indexable, including Dask.
00:57:20 - Question: what path should the images be in?
- Anywhere. You specify an absolute path in the
get_xfunction.
00:58:49 - Binary cross entropy cont
- The activations above aren't between 0 and 1, but you can use the [[Sigmoid]] function for that:
In [39]:
def binary_cross_entropy(inputs, targets):
inputs = inputs.sigmoid()
return torch.where(targets==1, 1-inputs, inputs).log().mean()
- Not that
logis applied here again so the training is "better behaved".- The function works on a single output or multiple.
- In PyTorch, the function is
F.binary_cross_entropy, or the module version isnn.BCELoss - The above loss functions don't use initial Sigmoid. Use
F.binary_cross_entropy_with_logitsornn.BCEWithLogitsLossif providing logits. - For single-label datasets like MNIST or Pets,
F.nll_loss(doesn't include Softmax) andF.cross_entropy(includes Softmax) are the equivalents.
01:03:29 - Multi-label metric
- Previously using error_rate (which is 1-accuracy), but that only works for single label dataset (argmax)
- In multi-label, can't use argmax because could have 0 or more labels.
- Need to introduce a
thresholdwhere above considered 1 and below 0.
- Need to introduce a
- In learner, you pass the metric as a function. To change
thresholdcan use a function calledpartial:
In [40]:
accuracy_multi_metric = partial(accuracy_multi, thresh=0.2)
In [41]:
learn = cnn_learner(dls, resnet50, metrics=accuracy_multi_metric)
Downloading: "https://download.pytorch.org/models/resnet50-0676ba61.pth" to /root/.cache/torch/hub/checkpoints/resnet50-0676ba61.pth
- Note that we don't need to loss func, as fastai figures it out:
In [42]:
learn.loss_func
Out[42]:
FlattenedLoss of BCEWithLogitsLoss()
In [43]:
learn.fine_tune(3, base_lr=3e-3, freeze_epochs=4)
| epoch | train_loss | valid_loss | accuracy_multi | time |
|---|---|---|---|---|
| 0 | 0.943449 | 0.704824 | 0.231494 | 00:31 |
| 1 | 0.823522 | 0.564629 | 0.289263 | 00:29 |
| 2 | 0.604577 | 0.197298 | 0.834024 | 00:30 |
| 3 | 0.359946 | 0.124828 | 0.940100 | 00:30 |
| epoch | train_loss | valid_loss | accuracy_multi | time |
|---|---|---|---|---|
| 0 | 0.132768 | 0.118560 | 0.939841 | 00:30 |
| 1 | 0.118844 | 0.106433 | 0.948586 | 00:30 |
| 2 | 0.096761 | 0.100886 | 0.952012 | 00:30 |
01:07:13 - Tuning threshold
- One way to find best threshold is to calculate accuracy at a range of thresholds and plot
In [44]:
preds, targs = learn.get_preds()
In [45]:
xs = torch.linspace(0.05, 0.95, 29)
accs = [accuracy_multi(preds, targs, thresh=i, sigmoid=False) for i in xs]
plt.plot(xs, accs)
Out[45]:
[<matplotlib.lines.Line2D at 0x7f370d65d090>]
- The fact it's a curve and not a bumpy function, is a good indication we're on right track.
01:09:09 - Regression (cont next notebook)
In [1]:
!pip install git+https://github.com/fastai/fastai.git &> /dev/null
In [2]:
from fastai.vision.all import *
01:09:09 - Regression (cont next notebook)
- Will learn how to "invent" applications using DataBlock
- Image regression: for an image, return some kinda of continuous value (age prediction is one example)
- In this example, use Biwi Kinect Head Pose Dataset to find the centre of a person's face.
In [3]:
path = untar_data(URLs.BIWI_HEAD_POSE)
In [4]:
Path.BASE_PATH = path
- 24 directories, each corresponding to person photographed.
In [5]:
path.ls().sorted()
Out[5]:
(#50) [Path('01'),Path('01.obj'),Path('02'),Path('02.obj'),Path('03'),Path('03.obj'),Path('04'),Path('04.obj'),Path('05'),Path('05.obj')...]
In [6]:
img_files = get_image_files(path)
- The label is a text file with the pose data. The filenames are the same as the first part of the image file name, ending in
pose.txt
In [7]:
def img2pose(x):
return Path(f'{str(x)[:-7]}pose.txt')
In [8]:
img2pose(img_files[0])
Out[8]:
Path('22/frame_00163_pose.txt')
In [9]:
open(img2pose(img_files[0])).read()
Out[9]:
'0.749067 0.0670679 0.659091 \n-0.248083 0.950873 0.185191 \n-0.614292 -0.30223 0.728905 \n\n-16.8291 56.2149 1048.09 \n\n'
- Fastai way to create PIL image is
PILImage.create
In [10]:
im = PILImage.create(img_files[0])
im.shape
Out[10]:
(480, 640)
In [11]:
im.to_thumb(160)
Out[11]:
- Note that in computer vision the dimensions are columns by rows.
- This function gets the head centre point:
In [12]:
cal = np.genfromtxt(path/'01'/'rgb.cal', skip_footer=6)
def get_ctr(f):
ctr = np.genfromtxt(img2pose(f), skip_header=3)
c1 = ctr[0] * cal[0][0]/ctr[2] + cal[0][2]
c2 = ctr[1] * cal[1][1]/ctr[2] + cal[1][2]
return tensor([c1, c2])
In [13]:
get_ctr(img_files[0])
Out[13]:
tensor([311.6877, 268.2660])
- That function can be passed to
get_ywhich labels each item
In [14]:
biwi = DataBlock(
blocks=(ImageBlock, PointBlock),
get_items=get_image_files,
get_y=get_ctr,
splitter=FuncSplitter(lambda o: o.parent.name=='13'),
batch_tfms=[*aug_transforms(size=(240, 320)),
Normalize.from_stats(*imagenet_stats)]
)
- Note that the validation set is an entire person. To enure that we don't trick ourselves into thinking we're generalising better than we are.
FuncSplitterallows you to split with a function.Normalizesubtracts the mean and divides by std.
In [15]:
dls = biwi.dataloaders(path)
dls.show_batch(max_n=9, figsize=(8, 6))
/opt/conda/lib/python3.7/site-packages/torch/_tensor.py:1023: UserWarning: torch.solve is deprecated in favor of torch.linalg.solveand will be removed in a future PyTorch release. torch.linalg.solve has its arguments reversed and does not return the LU factorization. To get the LU factorization see torch.lu, which can be used with torch.lu_solve or torch.lu_unpack. X = torch.solve(B, A).solution should be replaced with X = torch.linalg.solve(A, B) (Triggered internally at /pytorch/aten/src/ATen/native/BatchLinearAlgebra.cpp:760.) ret = func(*args, **kwargs)
In [16]:
xb, yb = dls.one_batch()
xb.shape, yb.shape
Out[16]:
(torch.Size([64, 3, 240, 320]), torch.Size([64, 1, 2]))
- Coordinates is a [1, 2] tensor (a single point).
01:16:47 - Image channels
- A colour picture is a rank-3 tensor. First axis is channels: red, green and blue.
In [17]:
im = image2tensor(PILImage.create(img_files[0]))
_, axs = subplots(1, 3)
for chan, ax, colour in zip(im, axs, ('Reds', 'Greens', 'Blues')):
show_image(255-chan, ax=ax, cmap=colour)
- A mini-batch of images is a rank-4 tensor.
01:17:45 - Training image regression model
- When we create a learner, we pass
y_rangewhich forces a range of targets.
def sigmoid_range(x, lo, hi):
return torch.sigmoid(x) * (hi-lo) + lo
In [18]:
learn = cnn_learner(dls, resnet18, y_range=(-1, 1))
Downloading: "https://download.pytorch.org/models/resnet18-f37072fd.pth" to /root/.cache/torch/hub/checkpoints/resnet18-f37072fd.pth
/opt/conda/lib/python3.7/site-packages/torch/nn/functional.py:718: UserWarning: Named tensors and all their associated APIs are an experimental feature and subject to change. Please do not use them for anything important until they are released as stable. (Triggered internally at /pytorch/c10/core/TensorImpl.h:1156.) return torch.max_pool2d(input, kernel_size, stride, padding, dilation, ceil_mode)
01:19:14 - Question: can you have more than 3 channels as input?
- Yes. Though pretrained models will need some changes to support this (copying layers for example)
01:20:13 - Training cont
- No loss function specified. fastai provides MSE by default for regression
In [19]:
dls.loss_func
Out[19]:
FlattenedLoss of MSELoss()
In [20]:
learn.lr_find()
/opt/conda/lib/python3.7/site-packages/fastai/callback/schedule.py:270: UserWarning: color is redundantly defined by the 'color' keyword argument and the fmt string "ro" (-> color='r'). The keyword argument will take precedence. ax.plot(val, idx, 'ro', label=nm, c=color)
Out[20]:
SuggestedLRs(valley=0.0014454397605732083)
In [21]:
lr = 0.002
learn.fine_tune(3, lr)
| epoch | train_loss | valid_loss | time |
|---|---|---|---|
| 0 | 0.124658 | 0.013659 | 01:53 |
| epoch | train_loss | valid_loss | time |
|---|---|---|---|
| 0 | 0.008415 | 0.000719 | 01:57 |
| 1 | 0.003397 | 0.000567 | 01:59 |
| 2 | 0.002295 | 0.000105 | 01:56 |
- Since it's squared error, we can get the square root to find average coord prediction error:
In [22]:
math.sqrt(0.000122)
Out[22]:
0.01104536101718726
In [23]:
learn.show_results(ds_idx=1, max_n=3, figsize=(6,8))
In [1]:
!pip install git+https://github.com/fastai/fastai.git &> /dev/null
01:25:14 - Collaborative filtering and latent factors
- What is collab filtering?
- On Netflix, you might be watching scifi movies made in 70s.
- Could find other people that have watched the same movies you've watched, and can recommend other films they've watched that you haven't.
- Foundation of this idea is that there are some features in items (ie movies) called latent factors that can be "uncovered"
01:28:09 - MovieLens
- Contains tens of millions of movie rankings:
In [2]:
from fastai.collab import *
from fastai.tabular.all import *
import pandas as pd
In [3]:
path = untar_data(URLs.ML_100k)
- Main table can be opened with pandas:
- Note that there's no header, so we provide our own column names.
In [4]:
ratings = pd.read_csv(
path/'u.data', delimiter='\t', header=None,
names=['user', 'movie', 'rating', 'timestamp'])
In [5]:
ratings.head()
Out[5]:
| user | movie | rating | timestamp | |
|---|---|---|---|---|
| 0 | 196 | 242 | 3 | 881250949 |
| 1 | 186 | 302 | 3 | 891717742 |
| 2 | 22 | 377 | 1 | 878887116 |
| 3 | 244 | 51 | 2 | 880606923 |
| 4 | 166 | 346 | 1 | 886397596 |
01:28:53 - Question: do you perform cross-validation in Deep Learning?
- Doing cross-validation or not is more a question about dataset size.
- Less common now due to bigger data sizes.
- Still used in Kaggle competitions where metric accuracy is quite important.
01:30:32 - Question: what other applications for collab filtering other than recommender system are there?
- Depends on how you define recommender system: diagnosis patients, figuring out what a person is likely to click on are sort of recommender systems.
01:31:19 - Question: how would you approach video classification?
- Usually approached as image classification per frame.
- Could also make the 4th axis time, but it would require a lot of memory.
01:33:20 - MovieLens cont.
ML_100kis a small subset of the full dataset (there's a 25M dataset too).- Can cross tab to get a better idea of what the dataset is:
01:35:32 - Latent factors
- Assume you have columns for each movie that described: how much action, how much drama and how much comedy on a scale of -1 to 1.
In [6]:
import numpy as np
In [7]:
last_movie = np.array([0.95, -0.9, -0.9])
- We could then describe a user based on their preferences for those rating categories:
In [8]:
user1 = np.array([0.9, -0.8, 0.6])
- We can then do element-wise multiplication to tell us how much they correlate:
In [9]:
(user1 * last_movie).sum()
Out[9]:
1.0350000000000001
- Multiplying 2 vectors and adding the results is called: [[Dot Product]]
- We could calculate the match between a less action-packed movie:
In [10]:
blue_valentine = np.array([-0.99, 0.99, 0.2])
In [11]:
(user1*blue_valentine).sum()
Out[11]:
-1.5630000000000002
- We don't actually know what the latent factors are in our dataset: but we can learn them.
01:37:51 - Learning latent factors
- We can visualise it in Excel, starting by setting latent factors to random values
- A matrix multiple of a row by column is identical of dot product of 2 vectors
- To do it in PyTorch, we start by replacing movie id with title. Just to make it easier to look at:
In [12]:
movies = pd.read_csv(
path/'u.item', delimiter='|', encoding='latin-1',
usecols=(0, 1), names=('movie', 'title'), header=None)
movies.head()
Out[12]:
| movie | title | |
|---|---|---|
| 0 | 1 | Toy Story (1995) |
| 1 | 2 | GoldenEye (1995) |
| 2 | 3 | Four Rooms (1995) |
| 3 | 4 | Get Shorty (1995) |
| 4 | 5 | Copycat (1995) |
mergeby default will merge on a shared column name.
In [13]:
ratings = ratings.merge(movies)
ratings.head()
Out[13]:
| user | movie | rating | timestamp | title | |
|---|---|---|---|---|---|
| 0 | 196 | 242 | 3 | 881250949 | Kolya (1996) |
| 1 | 63 | 242 | 3 | 875747190 | Kolya (1996) |
| 2 | 226 | 242 | 5 | 883888671 | Kolya (1996) |
| 3 | 154 | 242 | 3 | 879138235 | Kolya (1996) |
| 4 | 306 | 242 | 5 | 876503793 | Kolya (1996) |
- Can then build
DataLoadersobject from table.- Takes first column for user by default.
- Use
item_nameto specify item column
In [14]:
dls = CollabDataLoaders.from_df(ratings, item_name='title', bs=64)
dls.show_batch()
| user | title | rating | |
|---|---|---|---|
| 0 | 883 | City of Lost Children, The (1995) | 4 |
| 1 | 504 | Private Benjamin (1980) | 3 |
| 2 | 826 | Drop Zone (1994) | 3 |
| 3 | 333 | Fargo (1996) | 4 |
| 4 | 716 | Citizen Kane (1941) | 5 |
| 5 | 600 | Blade Runner (1982) | 5 |
| 6 | 252 | Twelve Monkeys (1995) | 4 |
| 7 | 429 | Vanya on 42nd Street (1994) | 3 |
| 8 | 152 | It Could Happen to You (1994) | 5 |
| 9 | 184 | Mrs. Doubtfire (1993) | 4 |
- Can represent movie and user latent factors as simple matrices:
In [15]:
n_users = len(dls.classes['user'])
n_movies = len(dls.classes['title'])
n_factors = 5
user_factors = torch.randn(n_users, n_factors)
movie_factors = torch.randn(n_movies, n_factors)
randnreturns normally distributed numbers.- Look up in index is not an operation deep learning models can do
- However, can represent look up in index as matrix product with a one-hot encoded vector
In [16]:
one_hot_3 = one_hot(3, n_users).float()
user_factors.t() @ one_hot_3
Out[16]:
tensor([-0.0480, 0.9249, 1.5718, 0.3341, 1.1332])
In [17]:
user_factors[3]
Out[17]:
tensor([-0.0480, 0.9249, 1.5718, 0.3341, 1.1332])
01:47:34 - Question: how different is collab filtering with sparse data compared to dense data?
- See fastai Linear Algebra course.
01:47:58 - Question: in practise, should we tune the number of latent factors?
- Yes. It's another hyperparameter that can be tuned.
01:48:16 - Embedding
- Doing matrix multiplication with a matrix of one-hot encoded vctors is an inefficient way to do the computation.
- Embedding:
- A computational shortcut
- Has the gradients of a one-hot matrix multiplication
- Has the speed of an array lookup
01:50:38 - Object-oriented programming
- In PyTorch, a model archiecture is a class that inherits from
nn.Module - Class syntax:
class Dog:
def __init__(self, name):
self.name
def speak(self):
print(f'{self.name} says wolf')
- The
__init__method is the classes constructor and is referred to as a "dunder method" (double underscore). - The syntax for inheritance is as follows:
class Hound(Dog):
def speak(self):
print(f'{self.name} the hound says wolf')
01:53:40 - Module in PyTorch
Moduleis a PyTorch class that fastai has customised:
In [18]:
class DotProduct(Module):
def __init__(self, n_users, n_movies, n_factors):
self.user_factors = Embedding(n_users, n_factors)
self.movie_factors = Embedding(n_movies, n_factors)
def forward(self, x):
users = self.user_factors(x[:,0])
movies = self.movie_factors(x[:,1])
return (users * movies).sum(dim=1)
- Function
forwardgets the user id and movie id as 2 columns. - We can use those to lookup the embeddings.
In [19]:
x, y = dls.one_batch()
x.shape
Out[19]:
torch.Size([64, 2])
- We can now use a standard Learner to train the model
In [20]:
model = DotProduct(n_users, n_movies, 50)
learn = Learner(dls, model, loss_func=MSELossFlat())
In [21]:
learn.fit_one_cycle(5, 5e-3)
| epoch | train_loss | valid_loss | time |
|---|---|---|---|
| 0 | 1.310729 | 1.323857 | 00:09 |
| 1 | 1.053884 | 1.083242 | 00:09 |
| 2 | 0.935609 | 0.968261 | 00:09 |
| 3 | 0.819087 | 0.883527 | 00:09 |
| 4 | 0.723053 | 0.870461 | 00:09 |
- To improve the results, we can make it better by forcing predictions between 0 and 5 using
sigmoid_range
In [22]:
class DotProduct(Module):
def __init__(self, n_users, n_movies, n_factors, y_range=(0, 5.5)):
self.user_factors = Embedding(n_users, n_factors)
self.movie_factors = Embedding(n_movies, n_factors)
self.y_range = y_range
def forward(self, x):
users = self.user_factors(x[:,0])
movies = self.movie_factors(x[:,1])
return sigmoid_range((users * movies).sum(dim=1), *self.y_range)
In [23]:
model = DotProduct(n_users, n_movies, 50)
learn = Learner(dls, model, loss_func=MSELossFlat())
In [24]:
learn.fit_one_cycle(5, 5e-3)
| epoch | train_loss | valid_loss | time |
|---|---|---|---|
| 0 | 0.993796 | 0.993444 | 00:09 |
| 1 | 0.866043 | 0.903248 | 00:09 |
| 2 | 0.656823 | 0.877895 | 00:09 |
| 3 | 0.483619 | 0.880073 | 00:09 |
| 4 | 0.381787 | 0.884775 | 00:09 |
01:59:02 - Bias term
- When we multiple the latent factors, it doesn't take account of the fact that some users rate movies badly. Or a movie is universally loved.
- We can add a single number for each movie and user, that it added to the result which takes that into account.
In [25]:
class DotProductBias(Module):
def __init__(self, n_users, n_movies, n_factors, y_range=(0, 5.5)):
self.user_factors = Embedding(n_users, n_factors)
self.user_bias = Embedding(n_users, 1)
self.movie_factors = Embedding(n_movies, n_factors)
self.movie_bias = Embedding(n_movies, 1)
self.y_range = y_range
def forward(self, x):
users = self.user_factors(x[:,0])
movies = self.movie_factors(x[:, 1])
res = (users * movies).sum(dim=1, keepdim=True)
res += self.user_bias(x[:,0]) + self.movie_bias(x[:,1])
return sigmoid_range(res, *self.y_range)
In [26]:
model = DotProduct(n_users, n_movies, 50)
learn = Learner(dls, model, loss_func=MSELossFlat())
learn.fit_one_cycle(5, 5e-3)
| epoch | train_loss | valid_loss | time |
|---|---|---|---|
| 0 | 1.024781 | 1.009018 | 00:09 |
| 1 | 0.846310 | 0.906241 | 00:09 |
| 2 | 0.660873 | 0.875660 | 00:09 |
| 3 | 0.484015 | 0.880105 | 00:09 |
| 4 | 0.379380 | 0.884129 | 00:09 |
02:01:05 - Regularisation
- We can see that the model appears to be overfitting quickly.
- With images, we have data augmentation. What can we use in collab filtering to allow training for longer? Regularisation
- To look at next week.