from jmd_imagescraper.core import *
from fastai.vision.all import *
Introduction
I am following along with Chapter 2 of [1].
Preliminaries
Install the jmd_imagescraper library using
!pip3 install jmd_imagescraper
I set FASTAI_HOME in my .bashrc so that datasets downloaded using Fastai are stored under a different location than the default. I do this because the default location, /home/kaushik/.fastai, is space constrained. Feel free to omit this step.
export FASTAI_HOME=/data/kaushik/.fastai
export PATH=$FASTAI_HOME:$PATH
Download and Clean Images
See utility function to download images, using the Duck Duck Go search engine, below (modified from here).
Code
def scrape_images(path, labels, search_suffix, erase_dir=True, max_images=20):
if erase_dir:
!rm -rf {path}
if not path.exists():
=True)
path.mkdir(parents
for some_label in labels:
\
duckduckgo_search(path, some_label,f'{some_label} {search_suffix}', max_results=max_images)
= get_image_files(path)
filenames = verify_images(filenames)
failed
map(Path.unlink);
failed.if failed != []:
= [filenames.remove(f) for f in failed]
_
# To avoid Transparency warnings, convert PNG images to RGBA
# https://forums.fast.ai/t/errors-when-training-the-bear-image-classification-model/83422/9
= L()
converted for image in filenames:
if '.png' in str(image):
= Image.open(image)
im # old file name before resaving
converted.append(image) "RGBA").save(f"{image}2.png")
im.convert(
map(Path.unlink); # delete originals
converted.
= len(get_image_files(path))
total_images print(f"After checking for issues, {total_images} (total) images remain.")
return path
Get 100 images for each kind of bear.
= 'grizzly','black','teddy'
labels = scrape_images(Path('/data/kaushik/bears'), labels, 'bear', max_images=100) path
Duckduckgo search: grizzly bear
Downloading results into /data/kaushik/bears/grizzly
Duckduckgo search: black bear
Downloading results into /data/kaushik/bears/black
Duckduckgo search: teddy bear
Downloading results into /data/kaushik/bears/teddy
After checking for issues, 300 (total) images remain.
Define the DataBlock
A DataBlock is a template for creating a DataLoader
= DataBlock(
bears =(ImageBlock, CategoryBlock),
blocks=get_image_files,
get_items=RandomSplitter(valid_pct=0.2,seed=0),
splitter=parent_label,
get_y=Resize(128)) item_tfms
Inspect a few items from the validation set
#collapse-output
= bears.dataloaders(path)
dls =4,nrows=1) dls.valid.show_batch(max_n
Inspect effect of Data Augmentations
#collapse-output
= bears.new(item_tfms=Resize(128), batch_tfms=aug_transforms(mult=2))
bears = bears.dataloaders(path)
dls =9, nrows=3, unique=True) dls.train.show_batch(max_n
Train Model
= bears.new(
bears =RandomResizedCrop(228, min_scale=0.5),
item_tfms=aug_transforms())
batch_tfms= bears.dataloaders(path) dls
= cnn_learner(dls, resnet18, metrics=error_rate)
learn 4) learn.fine_tune(
epoch | train_loss | valid_loss | error_rate | time |
---|---|---|---|---|
0 | 1.668255 | 0.410353 | 0.150000 | 00:03 |
epoch | train_loss | valid_loss | error_rate | time |
---|---|---|---|---|
0 | 0.551829 | 0.221585 | 0.100000 | 00:02 |
1 | 0.401239 | 0.252073 | 0.083333 | 00:02 |
2 | 0.327227 | 0.271660 | 0.083333 | 00:02 |
3 | 0.274876 | 0.251819 | 0.083333 | 00:02 |
= ClassificationInterpretation.from_learner(learn)
interp interp.plot_confusion_matrix()
5, nrows=1) interp.plot_top_losses(
Clean the data
from fastai.vision.widgets import *
The following will give us a UI that allows to mark images that are mislabeled (for relabeling) or completely wrong (for deletion).
= ImageClassifierCleaner(learn)
cleaner cleaner
So for each label we do the following:
First we choose the images to delete or relabel.
Second we run the following:
for idx in cleaner.delete() : cleaner.fns[idx].unlink()
for idx, cat in cleaner.change(): shutil.move(str(cleaner.fns[idx]), path/cat)
Items that need to be deleted or whose label needs to be changed
cleaner.delete(), cleaner.change()
((#4) [5,9,21,27],
(#11) [(0, 'black'),(1, 'black'),(2, 'black'),(3, 'black'),(11, 'black'),(14, 'black'),(15, 'black'),(16, 'black'),(17, 'black'),(19, 'black')...])
for idx in cleaner.delete() : cleaner.fns[idx].unlink()
for idx, cat in cleaner.change(): shutil.move(str(cleaner.fns[idx]), path/cat)
Retrain Model
= bears.new(
bears =RandomResizedCrop(228, min_scale=0.5),
item_tfms=aug_transforms())
batch_tfms= bears.dataloaders(path) dls
= cnn_learner(dls, resnet18, metrics=error_rate)
learn 4) learn.fine_tune(
epoch | train_loss | valid_loss | error_rate | time |
---|---|---|---|---|
0 | 1.947632 | 0.435558 | 0.189655 | 00:02 |
epoch | train_loss | valid_loss | error_rate | time |
---|---|---|---|---|
0 | 0.471205 | 0.192515 | 0.103448 | 00:02 |
1 | 0.352979 | 0.143951 | 0.086207 | 00:02 |
2 | 0.290317 | 0.153008 | 0.068965 | 00:02 |
3 | 0.261623 | 0.159573 | 0.051724 | 00:02 |
= ClassificationInterpretation.from_learner(learn)
interp interp.plot_confusion_matrix()
Performance on Black bears has improved but we are now misclassifying some Grizzlies as Black bears.
interp.print_classification_report()
precision recall f1-score support
black 0.86 1.00 0.92 18
grizzly 1.00 0.86 0.92 21
teddy 1.00 1.00 1.00 19
accuracy 0.95 58
macro avg 0.95 0.95 0.95 58
weighted avg 0.96 0.95 0.95 58
Save the model
'/data/kaushik/20210731/bear_v0/classifier.pkl') learn.export(
Load serialized model
= load_learner('/data/kaushik/20210731/bear_v0/classifier.pkl') learn_inf
Inference
def get_prediction(model, image_location):
return model.predict(image_location)
= '../test_images/black_bear.jpg'
test_black_bear_image_location = Image.open(test_black_bear_image_location)
im 128,128) im.to_thumb(
= get_prediction(learn_inf, test_black_bear_image_location)
pred, pred_idx, probs f'Prediction: {pred}; Probability: {probs[pred_idx]:.04f}'
'Prediction: black; Probability: 1.0000'
= '../test_images/grizzly.jpg'
test_grizzly_image_location = Image.open(test_grizzly_image_location)
im 128,128) im.to_thumb(
= get_prediction(learn_inf, test_grizzly_image_location)
pred, pred_idx, probs f'Prediction: {pred}; Probability: {probs[pred_idx]:.04f}'
'Prediction: grizzly; Probability: 1.0000'
Sadness
So far so good, test images of bears seem to be recognized perfectly. So now there is nowhere to go but downhill.
= '../test_images/mc.jpg'
test_mainecoon_image_location = Image.open(test_mainecoon_image_location)
im 128,128) im.to_thumb(
= get_prediction(learn_inf, test_mainecoon_image_location)
pred, pred_idx, probs f'Prediction: {pred}; Probability: {probs[pred_idx]:.04f}'
'Prediction: teddy; Probability: 0.8470'
So our classifier is very confident that the Maine Coon is a Teddy!
We work through how to tackle this issue by the use of multi-label classification in this post.
Model Demo
Chapter 2 of the book shows how to create a notebook app with Voila. However I recently ran into gradio which provides a neat way to quickly demo your model. The demo below will only run as long as my notebook is up.
Gradio has a paid hosted option which gives on the ability to have a permanent link.
import gradio as gr
def gradio_predict(img):
= learn_inf.predict(img)
pred, pred_idx, probs return {learn_inf.dls.vocab[i]: float(probs[i]) for i in range(len(probs))}
= gr.outputs.Label(num_top_classes=3) label
= gr.Interface(fn=gradio_predict,
iface =gr.inputs.Image(shape=(224,224)),
inputs=label)
outputs=True) iface.launch(share
The following screenshot shows the finished product. The UI allows one to upload an image, click submit and that’s it. Pretty neat!