The project that will be completed in this chapter is a duck detector. It will discriminate between three types of duck: white, green head, and rubber ducks.

At the time of writing, Bing Image Search is the best option we know of for finding and downloading images. It's free for up to 1,000 queries per month, and each query can download up to 150 images.

key = os.environ.get('AZURE_SEARCH_KEY', 'XXX')

Once you've set key, you can use search_images_bing. This function is provided by the small utils class included with the notebooks online. If you're not sure where a function is defined, you can just type it in your notebook to find out:

search_images_bing
<function fastbook.search_images_bing>
results = search_images_bing(key, 'duck')
ims = results.attrgot('contentUrl')
len(ims)
150

We've successfully downloaded the URLs of 150 ducks (or, at least, images that Bing Image Search finds for that search term).

dest = 'gdrive/MyDrive/images/green head.jpg'
download_url(ims[0], dest)
im = Image.open(dest)
im.to_thumb(128,128)
path = Path('gdrive/MyDrive/images/pedo bear')
if not path.exists():
    path.mkdir()
    path.mkdir(exist_ok=True)
    results = search_images_bing(key, 'pedo bear')
    download_images(path, urls=results.attrgot('contentUrl'))

Our folder has image files, as we'd expect:

fns = get_image_files(path)
fns
(#148) [Path('gdrive/MyDrive/images/pedo bear/00000003.jpg'),Path('gdrive/MyDrive/images/pedo bear/00000004.png'),Path('gdrive/MyDrive/images/pedo bear/00000001.png'),Path('gdrive/MyDrive/images/pedo bear/00000008.png'),Path('gdrive/MyDrive/images/pedo bear/00000011.jpg'),Path('gdrive/MyDrive/images/pedo bear/00000009.png'),Path('gdrive/MyDrive/images/pedo bear/00000006.png'),Path('gdrive/MyDrive/images/pedo bear/00000005.jpg'),Path('gdrive/MyDrive/images/pedo bear/00000010.png'),Path('gdrive/MyDrive/images/pedo bear/00000013.jpg')...]

This seems to have worked nicely, so let's use fastai's download_images to download all the URLs for each of our search terms. We'll put each in a separate folder:

Let's Get The Training Started!

duck_types = 'white', 'green head', 'rubber'
path = Path('gdrive/MyDrive/images/ducks')

if not path.exists():
    path.mkdir()
    for o in duck_types:
        dest = (path/o)
        dest.mkdir(exist_ok=True)
        results = search_images_bing(key, f'{o} duck')
        download_images(dest, urls=results.attrgot('contentUrl'))
fns = get_image_files(path)
fns

Often when we download files from the internet, there are a few that are corrupt. Let's check:

path = Path('gdrive/MyDrive/images/ducks')
(#444) [Path('gdrive/MyDrive/images/ducks/white/00000004.jpg'),Path('gdrive/MyDrive/images/ducks/white/00000001.jpg'),Path('gdrive/MyDrive/images/ducks/white/00000005.jpg'),Path('gdrive/MyDrive/images/ducks/white/00000007.jpg'),Path('gdrive/MyDrive/images/ducks/white/00000011.jpg'),Path('gdrive/MyDrive/images/ducks/white/00000010.jpg'),Path('gdrive/MyDrive/images/ducks/white/00000002.jpg'),Path('gdrive/MyDrive/images/ducks/white/00000000.jpg'),Path('gdrive/MyDrive/images/ducks/white/00000003.jpg'),Path('gdrive/MyDrive/images/ducks/white/00000009.jpg')...]
failed = verify_images(fns)
failed
(#3) [Path('gdrive/MyDrive/images/ducks/rubber/00000028.jpg'),Path('gdrive/MyDrive/images/ducks/rubber/00000061.jpg'),Path('gdrive/MyDrive/images/ducks/rubber/00000081.jpg')]

To remove all the failed images, you can use unlink on each of them. Note that, like most fastai functions that return a collection, verify_images returns an object of type L, which includes the map method. This calls the passed function on each element of the collection:

failed.map(Path.unlink)

From Data to DataLoaders

We need to tell fastai at least four things:

  • What kinds of data we are working with
  • How to get the list of items
  • How to label these items
  • How to create the validation set

With data block API you can fully customize every stage of the creation of your DataLoaders. Here is what we need to create a DataLoaders for the dataset that we just downloaded:

ducks = DataBlock(
    blocks=(ImageBlock, CategoryBlock), 
    get_items=get_image_files, 
    splitter=RandomSplitter(valid_pct=0.2, seed=42),
    get_y=parent_label,
    item_tfms=Resize(128))

DataBlock is like a template for creating a DataLoaders. We still need to tell fastai the actual source of our data—in this case, the path where the images can be found:

dls = ducks.dataloaders(path)
dls.valid.show_batch(max_n=4, nrows=1)

Data Augmentation

ducks = ducks.new(item_tfms=Resize(224), batch_tfms=aug_transforms(mult=2))
dls = ducks.dataloaders(path)
dls.train.show_batch(max_n=8, nrows=2, unique=True)
/usr/local/lib/python3.7/dist-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)

Learner and Confusion Matrix

learn = cnn_learner(dls, resnet18, metrics=accuracy)
learn.fine_tune(4)
Downloading: "https://download.pytorch.org/models/resnet18-f37072fd.pth" to /root/.cache/torch/hub/checkpoints/resnet18-f37072fd.pth

/usr/local/lib/python3.7/dist-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)
epoch train_loss valid_loss accuracy time
0 1.509567 0.091312 0.965909 00:17
/usr/local/lib/python3.7/dist-packages/PIL/Image.py:960: UserWarning: Palette images with Transparency expressed in bytes should be converted to RGBA images
  "Palette images with Transparency expressed in bytes should be "
/usr/local/lib/python3.7/dist-packages/PIL/Image.py:960: UserWarning: Palette images with Transparency expressed in bytes should be converted to RGBA images
  "Palette images with Transparency expressed in bytes should be "
epoch train_loss valid_loss accuracy time
0 0.328706 0.051048 0.977273 00:17
1 0.249838 0.024654 0.988636 00:17
2 0.193008 0.016960 1.000000 00:18
3 0.154048 0.015811 1.000000 00:17
/usr/local/lib/python3.7/dist-packages/PIL/Image.py:960: UserWarning: Palette images with Transparency expressed in bytes should be converted to RGBA images
  "Palette images with Transparency expressed in bytes should be "
/usr/local/lib/python3.7/dist-packages/PIL/Image.py:960: UserWarning: Palette images with Transparency expressed in bytes should be converted to RGBA images
  "Palette images with Transparency expressed in bytes should be "
/usr/local/lib/python3.7/dist-packages/PIL/Image.py:960: UserWarning: Palette images with Transparency expressed in bytes should be converted to RGBA images
  "Palette images with Transparency expressed in bytes should be "
/usr/local/lib/python3.7/dist-packages/PIL/Image.py:960: UserWarning: Palette images with Transparency expressed in bytes should be converted to RGBA images
  "Palette images with Transparency expressed in bytes should be "
/usr/local/lib/python3.7/dist-packages/PIL/Image.py:960: UserWarning: Palette images with Transparency expressed in bytes should be converted to RGBA images
  "Palette images with Transparency expressed in bytes should be "
/usr/local/lib/python3.7/dist-packages/PIL/Image.py:960: UserWarning: Palette images with Transparency expressed in bytes should be converted to RGBA images
  "Palette images with Transparency expressed in bytes should be "
/usr/local/lib/python3.7/dist-packages/PIL/Image.py:960: UserWarning: Palette images with Transparency expressed in bytes should be converted to RGBA images
  "Palette images with Transparency expressed in bytes should be "
interp = ClassificationInterpretation.from_learner(learn)
interp.plot_confusion_matrix()

Turning Your Model into an Online Application

learn.export()
path = Path()
path.ls(file_exts='.pkl')
(#1) [Path('export.pkl')]
%mv export.pkl gdrive/MyDrive/
learn_inf = load_learner(path/'gdrive/MyDrive/export.pkl')
learn_inf.dls.vocab
['green head', 'rubber', 'white']

Creating a Notebook App from the Model

With ipywidgets, we can build up our GUI step by step. We will use this approach to create a simple image classifier. First, we need a file upload widget:

btn_upload = widgets.FileUpload()
btn_upload

image.png

img = PILImage.create(btn_upload.data[-1])
img

We can use an Output widget to display it:

out_pl = widgets.Output()
out_pl.clear_output()
with out_pl: display(img.to_thumb(128,128))
out_pl

Then we can get our predictions:

pred,pred_idx,probs = learn_inf.predict(img)

and use a Label to display them:

lbl_pred = widgets.Label()
lbl_pred.value = f'Prediction: {pred}; Probability: {probs[pred_idx]:.04f}'
lbl_pred

Label(value='Prediction: white; Probability: 1.0000')

We'll need a button to do the classification. It looks exactly like the upload button:

btn_run = widgets.Button(description='Classify')
btn_run

image.png

We'll also need a click event handler; that is, a function that will be called when it's pressed.

def on_click_classify(change):
    img = PILImage.create(btn_upload.data[-1])
    out_pl.clear_output()
    with out_pl: display(img.to_thumb(128,128))
    pred,pred_idx,probs = learn_inf.predict(img)
    lbl_pred.value = f'Prediction: {pred}; Probability: {probs[pred_idx]:.04f}'

btn_run.on_click(on_click_classify)

You can test the button now by pressing it, and you should see the image and predictions update automatically!

We can now put them all in a vertical box (VBox) to complete our GUI:

VBox([widgets.Label('Select your duck!'), 
      btn_upload, btn_run, out_pl, lbl_pred])

image.png

DeployingYour Notebook into a Real App

For at least the initial prototype of your application, and for any hobby projects that you want to show off, you can easily host them for free. In early 2020 the simplest (and free!) approach is to use Binder.

Get Writing!

Rachel Thomas, cofounder of fast.ai, wrote in the article "Why You (Yes, You) Should Blog":

The top advice I would give my younger self would be to start blogging sooner. Here are some reasons to blog:

  • It’s like a resume, only better. I know of a few people who have had blog posts lead to job offers!
  • Helps you learn. Organizing knowledge always helps me synthesize my own ideas. One of the tests of whether you understand something is whether you can explain it to someone else. A blog post is a great way to do that.
  • I’ve gotten invitations to conferences and invitations to speak from my blog posts. I was invited to the TensorFlow Dev Summit (which was awesome!) for writing a blog post about how I don’t like TensorFlow.
  • Meet new people. I’ve met several people who have responded to blog posts I wrote.
  • Saves time. Any time you answer a question multiple times through email, you should turn it into a blog post, which makes it easier for you to share the next time someone asks.

Perhaps her most important tip is this:

You are best positioned to help people one step behind you. The material is still fresh in your mind. Many experts have forgotten what it was like to be a beginner (or an intermediate) and have forgotten why the topic is hard to understand when you first hear it. The context of your particular background, your particular style, and your knowledge level will give a different twist to what you’re writing about.

The full details on how to set up a blog are in fastpages. If you don't have a blog already, take a look at that now, because we've got a really great approach set up for you to start blogging for free, with no ads—and you can even use Jupyter Notebook!