Skip to content

Commit a421ccd

Browse files
Merge pull request #7 from UPstartDeveloper/polish
Refactor README, add docker-compose.yml
2 parents d36d389 + bff54a1 commit a421ccd

File tree

3 files changed

+41
-311
lines changed

3 files changed

+41
-311
lines changed

Dockerfile

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
FROM python:3.6-slim
2-
COPY app/main.py /deploy/
3-
COPY app/config.yaml /deploy/
4-
WORKDIR /deploy/
2+
COPY app/ /app/
3+
WORKDIR /
54
RUN apt update
65
RUN apt install -y git
76
RUN apt-get install -y libglib2.0-0

README.md

Lines changed: 29 additions & 308 deletions
Original file line numberDiff line numberDiff line change
@@ -1,325 +1,46 @@
1-
# Fast_Image_Classification
1+
# *The* Fire Detection API
2+
![Project cover image](https://i.postimg.cc/1RNPLFF3/Screen-Shot-2021-09-02-at-9-58-41-AM.png)
23

3-
Code for https://towardsdatascience.com/a-step-by-step-tutorial-to-build-and-deploy-an-image-classification-api-95fa449f0f6a
4+
Deep learning has the power to potentially save millions of dollars (and more importantly, lives) in places like California where the annual "fire season" arrives every Fall.
45

5-
### Data
6-
Download data : <https://github.com/CVxTz/ToyImageClassificationDataset>
6+
We built this API to show how the technology can fight this and other crises, and inspire our students to do the same.
77

8-
### Docker
8+
## Getting Started
99

10-
```sudo docker build -t img_classif .```
10+
### Use the API
11+
To classify your own images, you can use the live API: use the link [here](https://fire-detection-api.herokuapp.com/docs) to read the documentation and send requests.
1112

12-
```sudo docker run -p 8080:8080 img_classif```
13+
### Running Locally
14+
You can download this repository and run it using [Docker](https://www.docker.com/get-started):
1315

14-
```time curl -X POST "http://127.0.0.1:8080/scorefile/" -H "accept: application/json" -H "Content-Type: multipart/form-data" -F "file=@337px-Majestic_Twilight.jpg"```
16+
```docker compose up```
1517

16-
## Description
18+
Alternatively, you can also make a virtual environment and run it using the `uvicorn` package:
1719

18-
Our objective in this small project is to build an image classification API from scratch.
19-
We are going to go through all the steps needed to achieve this objective :
20+
```
21+
$ python3 -m venv env # creates a virtualenv
22+
$ source env/bin/activate # now you're in the virtualenv
23+
$ uvicorn app.main:app --reload # run the app
2024
21-
* Data annotation (with Unsplash API + Labelme )
25+
```
2226

23-
* Model Training ( With Tensorflow )
27+
## The Data and the Model
28+
The image dataset and model used for the production API will be documented on the [Releases](https://github.com/UPstartDeveloper/Fire-Detection-API/releases) page of this repository.
2429

25-
* Making the API ( With Uvicorn and FastApi )
30+
## Making Your Own Deep Learning API
2631

27-
* Deploying the API on a remote server ( With Docker and Google Cloud Platform )
32+
TBD
2833

29-
## Data Annotation :
34+
## Deploying to Heroku
3035

31-
One of the most important parts of any machine learning project is the quality and quantity of the annotated data. It is one of the key factors that will influence the quality of the predictions when the API is deployed.
36+
TBD
37+
## Stretch Challenges
3238

33-
In this project we will try to classify an input image into four classes :
39+
In this project, we've worked with different tools like Tensorflow, Docker, FastAPI and Heroku. The next steps would be to two-fold:
3440

35-
* City
41+
- For the **modelling** engineers: how would you improve the neural networks performance?
42+
- For the **MLOps** engineers: how would you improve the performance and scalability of the REST API in production?
3643

37-
* Beach
38-
39-
* Sunset
40-
41-
* Trees/Forest
42-
43-
I choose those classes because it is easy to find tons of images representing them online. We use those classes to define a multi-label classification problem :
44-
45-
![Examples of inputs and targets / Images from [https://unsplash.com/](https://unsplash.com/)](https://cdn-images-1.medium.com/max/2000/1*buGA2Qk4KXqJMq5Xu5gffg.png)*Examples of inputs and targets / Images from [https://unsplash.com/](https://unsplash.com/)*
46-
47-
Now that we have defined the problem we want to solve we need to get a sufficient amount of labeled samples for training and evaluation.
48-
To do that we will first use the [Unsplash](https://unsplash.com/) API to get URLs of images given multiple search queries.
49-
50-
# First install [https://github.com/yakupadakli/python-unsplash](https://github.com/yakupadakli/python-unsplash)
51-
# Unsplash API [https://unsplash.com/documentation](https://unsplash.com/documentation)
52-
import json
53-
import os
54-
55-
from unsplash.api import Api
56-
from unsplash.auth import Auth
57-
58-
with open('tokens.json', 'r') as f:
59-
data = json.load(f)
60-
61-
client_id = data['client_id']
62-
client_secret = data['client_secret']
63-
64-
redirect_uri = ""
65-
code = ""
66-
67-
keyword = 'beach'
68-
69-
auth = Auth(client_id, client_secret, redirect_uri, code=code)
70-
api = Api(auth)
71-
72-
photos = api.search.photos(keyword, per_page=1000, page=i)['results']
73-
74-
for photo in photos:
75-
print(photo)
76-
print(photo.id)
77-
print(photo.urls)
78-
print(photo.urls.small)
79-
80-
We would try to get image URLs that are related to our target classes plus some other random images that will serve as negative examples.
81-
82-
The next step is to go through all the images and assign a set of labels to each one of them, like what is shown in the figure above. For this it is always easier to use annotations tools that are designed for this task like [LabelMe,](https://github.com/wkentaro/labelme) it is a python library that you can run easily from the command line:
83-
84-
labelme . -flags labels.txt
85-
86-
![Labelme user interface](https://cdn-images-1.medium.com/max/2000/1*4iIY7gR7ZYEc-qEW3HJmLg.png)*Labelme user interface*
87-
88-
Using Labelme I labeled around a thousand images and made the urls+labels available here: [https://github.com/CVxTz/ToyImageClassificationDataset](https://github.com/CVxTz/ToyImageClassificationDataset)
89-
90-
## Model
91-
92-
Now that we have the labeled samples we can try building a classifier using Tensorflow. We will use MobileNet_V2 as the backbone of the classifier since it is fast and less likely to over-fit given the tiny amount of labeled samples we have, you can easily use it by importing it from keras_applications :
93-
94-
from tensorflow.keras.applications import MobileNetV2
95-
96-
base_model = MobileNetV2(include_top=False, input_shape=input_shape, weights=weights)
97-
98-
Since it is a multi-label classification problem with four classes, we will have an output layer of four neurons with the Sigmoid activation ( given an example, we can have multiple neurons active or no neuron active as the target)
99-
100-
### Transfer learning
101-
102-
One commonly used trick to tackle the lack of labeled samples is to use transfer learning. It is when you transfer some of the weights learned from a source task ( like image classification with a different set of labels) to your target task as the starting point of your training. This allows for a better initialization compared to starting from random and allows for reusing some of the representations learned on the source task for our multi-label classification.
103-
104-
Here we will transfer the weights that resulted from training in ImageNet. Doing this is very easy when using Tensorflow+Keras for MobileNet_V2, you just need to specify weights=”imagenet” when creating an instance of MobileNetV2
105-
106-
base_model = MobileNetV2(include_top=False, input_shape=input_shape, weights="imagenet")
107-
108-
### Data augmentation
109-
110-
Another trick to improve performance when having a small set of annotated samples is to do data augmentation. It is the process of applying random perturbations that preserve the label information ( a picture of a city after the perturbations still looks like a city ). Some common transformations are vertical mirroring, salt and pepper noise or blurring.
111-
112-
![Data augmentation examples / Image from [https://unsplash.com/](https://unsplash.com/)](https://cdn-images-1.medium.com/max/3710/1*BNhj5p5uTfwF9yaeRDuBUQ.png)*Data augmentation examples / Image from [https://unsplash.com/](https://unsplash.com/)*
113-
114-
To achieve this we use a python package called imgaug and define a sequence of transformation along with their amplitude :
115-
116-
sometimes = **lambda **aug: iaa.Sometimes(0.1, aug)
117-
seq = iaa.Sequential(
118-
[
119-
sometimes(iaa.Affine(scale={**"x"**: (0.8, 1.2)})),
120-
sometimes(iaa.Fliplr(p=0.5)),
121-
sometimes(iaa.Affine(scale={**"y"**: (0.8, 1.2)})),
122-
sometimes(iaa.Affine(translate_percent={**"x"**: (-0.2, 0.2)})),
123-
sometimes(iaa.Affine(translate_percent={**"y"**: (-0.2, 0.2)})),
124-
sometimes(iaa.Affine(rotate=(-20, 20))),
125-
sometimes(iaa.Affine(shear=(-20, 20))),
126-
sometimes(iaa.AdditiveGaussianNoise(scale=0.07 * 255)),
127-
sometimes(iaa.GaussianBlur(sigma=(0, 3.0))),
128-
],
129-
random_order=**True**,
130-
)
131-
132-
### Training
133-
134-
We split the dataset into two folds, training and validation and use the binary_crossentropy as our target along with the binary_accuracy as the evaluation metric.
135-
136-
We run the training from the command line after updating some configuration files :
137-
138-
# data_config.yaml for defnining the classes and input size**
139-
input_shape**: [null, null, 3]
140-
**resize_shape**: [224, 224]
141-
**images_base_path**: **'../example/data/'
142-
targets**: [**'beach'**, **'city'**, **'sunset'**, **'trees'**]
143-
**image_name_col**: **'name'**
144-
145-
# training_config.yaml for defining some training parameters**
146-
use_augmentation**: true
147-
**batch_size**: 32
148-
**epochs**: 1000
149-
**initial_learning_rate**: 0.0001
150-
**model_path**: **"image_classification.h5"**
151-
152-
Then running the training script :
153-
154-
**export PYTHONPATH=$PYTHONPATH:~/PycharmProjects/FastImageClassification/**
155-
156-
**python train.py --csv_path "../example/data.csv" \
157-
--data_config_path "../example/data_config.yaml" \
158-
--training_config_path "../example/training_config.yaml"**
159-
160-
![](https://cdn-images-1.medium.com/max/2048/1*azNZV0IqtYNajUzboMaYDw.gif)
161-
162-
We end up with a validation binary accuracy of **94%**
163-
164-
## Making the API
165-
166-
We will be using FastAPI to expose a predictor through an easy to use API that can take as input an image file and outputs a JSON with the classification scores for each class.
167-
168-
First, we need to write a Predictor class that can easily load a tensorflow.keras model and have a method to classify an image that is in the form of a file object.
169-
170-
**class **ImagePredictor:
171-
**def **__init__(
172-
self, model_path, resize_size, targets, pre_processing_function=preprocess_input
173-
):
174-
self.model_path = model_path
175-
self.pre_processing_function = pre_processing_function
176-
self.model = load_model(self.model_path)
177-
self.resize_size = resize_size
178-
self.targets = targets
179-
180-
@classmethod
181-
**def **init_from_config_path(cls, config_path):
182-
**with **open(config_path, **"r"**) **as **f:
183-
config = yaml.load(f, yaml.SafeLoader)
184-
predictor = cls(
185-
model_path=config[**"model_path"**],
186-
resize_size=config[**"resize_shape"**],
187-
targets=config[**"targets"**],
188-
)
189-
**return **predictor
190-
191-
@classmethod
192-
**def **init_from_config_url(cls, config_path):
193-
**with **open(config_path, **"r"**) **as **f:
194-
config = yaml.load(f, yaml.SafeLoader)
195-
196-
download_model(
197-
config[**"model_url"**], config[**"model_path"**], config[**"model_sha256"**]
198-
)
199-
200-
**return **cls.init_from_config_path(config_path)
201-
202-
**def **predict_from_array(self, arr):
203-
arr = resize_img(arr, h=self.resize_size[0], w=self.resize_size[1])
204-
arr = self.pre_processing_function(arr)
205-
pred = self.model.predict(arr[np.newaxis, ...]).ravel().tolist()
206-
pred = [round(x, 3) **for **x **in **pred]
207-
**return **{k: v **for **k, v **in **zip(self.targets, pred)}
208-
209-
**def **predict_from_file(self, file_object):
210-
arr = read_from_file(file_object)
211-
**return **self.predict_from_array(arr)
212-
213-
We can use a configuration file to instantiate a predictor object that has all the parameters to do predictions and will download the model from the GitHub repository of the project :
214-
215-
**# config.yaml
216-
resize_shape**: [224, 224]
217-
**targets**: [**'beach'**, **'city'**, **'sunset'**, **'trees'**]
218-
**model_path**: **"image_classification.h5"
219-
model_url**: **"https://github.com/CVxTz/FastImageClassification/releases/download/v0.1/image_classification.h5"
220-
model_sha256**: **"d5cd9082651faa826cab4562f60e3095502286b5ea64d5b25ba3682b66fbc305"**
221-
222-
After doing all of this, the main file of our API becomes trivial when using FastAPI :
223-
224-
**from **fastapi **import **FastAPI, File, UploadFile
225-
226-
**from **fast_image_classification.predictor **import **ImagePredictor
227-
228-
app = FastAPI()
229-
230-
predictor_config_path = **"config.yaml"**
231-
232-
predictor = ImagePredictor.init_from_config_url(predictor_config_path)
233-
234-
235-
@app.post(**"/scorefile/"**)
236-
**def **create_upload_file(file: UploadFile = File(...)):
237-
**return **predictor.predict_from_file(file.file)
238-
239-
We can now run the app with a single command :
240-
241-
uvicorn main:app --reload
242-
243-
This gives us access to Swagger UI where we can try out our API on a new file.
244-
245-
![[http://127.0.0.1:8080/docs](http://127.0.0.1:8000/docs)](https://cdn-images-1.medium.com/max/2898/1*ka58fs87P-ptDS8aciP4Gw.png)*[http://127.0.0.1:8080/docs](http://127.0.0.1:8000/docs)*
246-
247-
![Photo by [Antonio Resendiz](https://unsplash.com/@antonioresendiz_?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/s/photos/city?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)](https://cdn-images-1.medium.com/max/5606/1*uPdQuS0Y4Esz5vYZk13Npw.jpeg)*Photo by [Antonio Resendiz](https://unsplash.com/@antonioresendiz_?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/s/photos/city?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)*
248-
249-
Uploading the image above gives up the following output :
250-
251-
{**
252-
**"beach":** 0**,**
253-
**"city":** 0**.**999**,**
254-
**"sunset":** 0**.**005**,**
255-
**"trees":** 0
256-
**}
257-
258-
Which is the expected output!
259-
260-
We can also send a request via curl and time it :
261-
262-
time curl -X POST "[http://127.0.0.1:8080/scorefile/](http://127.0.0.1:8000/scorefile/)" -H "accept: application/json" -H "Content-Type: multipart/form-data" -F "file=[@antonio](http://twitter.com/antonio)-resendiz-VTLqQe4Ej8I-unsplash.jpg;type=image/jpeg"
263-
264-
>> {"beach":0.0,"city":0.999,"sunset":0.005,"trees":0.0}
265-
>> real 0m0.209s
266-
>> user 0m0.012s
267-
>> sys 0m0.008s
268-
269-
## Deploying the App
270-
271-
### Docker
272-
273-
Its easier to deploy an app if it is inside a container like Docker.
274-
275-
We will create a Dockerfile with all the instructions needed to run our app after installing the correct environment :
276-
277-
**FROM** python:3.6-slim
278-
**COPY** app/main.py /deploy/
279-
**COPY** app/config.yaml /deploy/
280-
**WORKDIR** /deploy/
281-
**RUN** apt update
282-
**RUN** apt install -y git
283-
**RUN** apt-get install -y libglib2.0-0
284-
**RUN** pip install git+https://github.com/CVxTz/FastImageClassification
285-
**EXPOSE** 8080
286-
287-
**ENTRYPOINT** uvicorn main:app --host 0.0.0.0 --port 8080
288-
289-
Install Docker :
290-
291-
sudo apt install docker.io
292-
293-
Then we can run the Docker build :
294-
295-
sudo docker build -t img_classif .
296-
297-
We finally run the container while mapping the port 8080 of container to that of the host :
298-
299-
sudo docker run -p 8080:8080 img_classif .
300-
301-
### Deploying on a remote server
302-
303-
I tried to do this on an ec2 instance from AWS but the ssh command line was clunky and the terminal would freeze at the last command, no idea why. So I decided to do the deployment using Google Cloud Platform’s App engine. Link to a more detailed tutorial on the subject [here](https://blog.machinebox.io/deploy-docker-containers-in-google-cloud-platform-4b921c77476b).
304-
305-
* Create a google cloud platform account
306-
307-
* install gcloud
308-
309-
* create project project_id
310-
311-
* clone [https://github.com/CVxTz/FastImageClassification](https://github.com/CVxTz/FastImageClassification) and call :
312-
313-
cd FastImageClassification
314-
315-
gcloud config set project_id
316-
317-
gcloud app deploy app.yaml -v v1
318-
319-
The last command will take a while but … Voilaaa!
320-
321-
![](https://cdn-images-1.medium.com/max/3702/1*UcHnFytWaAIpeM-ZL_3Dng.png)
322-
323-
## Conclusion
324-
325-
In this project, we built and deployed machine-learning powered image classification API from scratch using Tensorflow, Docker, FastAPI and Google Cloud Platform‘s App Engine. All those tools made the whole process straightforward and relatively easy. The next step would be to explore questions related to security and performance when handling a large number of queries.
44+
## Credits and Resources
45+
1. This *Towards Data Science* [blog](https://towardsdatascience.com/a-step-by-step-tutorial-to-build-and-deploy-an-image-classification-api-95fa449f0f6a) by Youness Mansar will give you a little more detail on how you can build a deployment-driven deep learning project (using the Google Cloud Platform's App Engine).
46+
2. Another [blog](https://towardsdatascience.com/how-to-deploy-your-fastapi-app-on-heroku-for-free-8d4271a4ab9#beb1) by Shinichi Okada in *Towards Data Science* will give more details how to deploy FastAPI applications (such as this repo!) on Heroku specifically.

docker-compose.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
version: '3.7'
2+
3+
services:
4+
web:
5+
build:
6+
context: ./
7+
dockerfile: Dockerfile
8+
# command: uvicorn app.main:app
9+
ports:
10+
- "8080:8080"

0 commit comments

Comments
 (0)