serveml 0.3.0
serveml is a machine learning serving tool
License: Apache
Requires Python: >=3.6

# serveml

[![Build Status](https://travis-ci.org/gfalcone/serveml.svg?branch=master)](https://travis-ci.org/gfalcone/serveml)

`serveml` is a Python library that helps you package your Machine Learning model easily into a REST API.

The idea behind `serveml` is to define a set of generic endpoints to make predictions easily !

## Requirements

- Python 3.6+
- [FastAPI](https://fastapi.tiangolo.com/) (for the API part)
- [MLflow](https://mlflow.org/) (for model loading)
- [Uvicorn](https://www.uvicorn.org/) (to run api)


## Installation

```bash
pip install serveml
```

## Documentation

You can find the full documentation here : https://gfalcone.github.io/serveml/

## How to use ? 

### Prerequisites 

In order to run the examples we put, you'll need an MLflow server running. 

As we do not expect you to have already this in place, we set up a docker container in order to speed things up.

You'll need to do the following things to set up MLflow on your local machine : 

```bash
git clone https://github.com/gfalcone/serveml
cd serveml
mkdir -p /tmp/mlflow
docker-compose build
docker-compose up
```

### Training

First of all, you need to have a model already trained and registered in MlFlow

Luckily for you, we already have a set of examples that you can already use.

Let's say you have a scikit-learn model, like this one (taken from examples/serving/sklearn.py): 


```python
"""
Example taken from https://github.com/mlflow/mlflow/blob/master/examples/sklearn_elasticnet_wine/train.py
"""
import warnings
import sys

import pandas as pd
import numpy as np
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
from sklearn.model_selection import train_test_split
from sklearn.linear_model import ElasticNet

import mlflow
import mlflow.sklearn

import logging

logging.basicConfig(level=logging.WARN)
logger = logging.getLogger(__name__)


def eval_metrics(actual, pred):
    rmse = np.sqrt(mean_squared_error(actual, pred))
    mae = mean_absolute_error(actual, pred)
    r2 = r2_score(actual, pred)
    return rmse, mae, r2


if __name__ == "__main__":
    mlflow.set_tracking_uri("http://localhost:5000")
    warnings.filterwarnings("ignore")
    np.random.seed(40)

    # Read the wine-quality csv file from the URL
    csv_url = "http://archive.ics.uci.edu/ml/machine-learning-databases/wine-quality/winequality-red.csv"
    try:
        data = pd.read_csv(csv_url, sep=";")
    except Exception as e:
        logger.exception(
            "Unable to download training & test CSV, check your internet connection. Error: %s",
            e,
        )

    # Split the data into training and test sets. (0.75, 0.25) split.
    train, test = train_test_split(data)

    # The predicted column is "quality" which is a scalar from [3, 9]
    train_x = train.drop(["quality"], axis=1)
    test_x = test.drop(["quality"], axis=1)
    train_y = train[["quality"]]
    test_y = test[["quality"]]

    alpha = float(sys.argv[1]) if len(sys.argv) > 1 else 0.5
    l1_ratio = float(sys.argv[2]) if len(sys.argv) > 2 else 0.5

    experiment_name = 'test_sklearn'
    if mlflow.get_experiment_by_name(experiment_name) is None:
        mlflow.create_experiment(experiment_name)

    with mlflow.start_run(experiment_id=1):
        lr = ElasticNet(alpha=alpha, l1_ratio=l1_ratio, random_state=42)
        lr.fit(train_x, train_y)

        predicted_qualities = lr.predict(test_x)

        (rmse, mae, r2) = eval_metrics(test_y, predicted_qualities)

        print("Elasticnet model (alpha=%f, l1_ratio=%f):" % (alpha, l1_ratio))
        print("  RMSE: %s" % rmse)
        print("  MAE: %s" % mae)
        print("  R2: %s" % r2)

        mlflow.log_param("alpha", alpha)
        mlflow.log_param("l1_ratio", l1_ratio)
        mlflow.log_metric("rmse", rmse)
        mlflow.log_metric("r2", r2)
        mlflow.log_metric("mae", mae)

        mlflow.sklearn.log_model(
            lr, "model", registered_model_name="sklearn_model"
        )
```

You can run it with : 

```bash
python -m examples.training.sklearn
```

### Serving

We can then define the API this way (taken from examples/serving/sklearn.py): 

```python
from serveml.api import ApiBuilder
from serveml.inputs import BasicInput
from serveml.loader import load_mlflow_model
from serveml.predictions import GenericPrediction

# load model
model = load_mlflow_model(
    # MlFlow model path
    'models:/sklearn_model/1',
    # MlFlow Tracking URI (optional)
    'http://localhost:5000',
)


# Implement deserializer for input data
class WineComposition(BasicInput):
    alcohol: float
    chlorides: float
    citric_acid: float
    density: float
    fixed_acidity: float
    free_sulfur_dioxide: int
    pH: float
    residual_sugar: float
    sulphates: float
    total_sulfur_dioxide: int
    volatile_acidity: int


# implement application
app = ApiBuilder(GenericPrediction(model), WineComposition).build_api()
```

And then run it with : 

```bash
uvicorn examples.serving.sklearn:app --host 0.0.0.0
```

You can now access your API's documentation, generated by [redoc](https://github.com/Redocly/redoc) on [localhost:8000/redoc]() or  access your API with Swagger on [localhost:8000/docs]() :

![API](https://github.com/gfalcone/serveml/blob/master/docs/images/mlserve-example.gif)

Don't forget to exit the Docker container to shut down MLflow when you're done :

```bash
docker-compose down
```

## Testing

### Unit tests

To run unit tests, do the following : 

```bash
docker build --tag=serveml -f Dockerfile .
```

### Documentation

If you want to look how the documentation will be rendered after making changes to it : 

```bash
pip install -r requirements-doc.txt
mkdocs serve
```

## Contributing

If you wish to make some changes, we are obviously open to Pull Requests. 

Please not that in order for your PR to be merged the following points are mandatory : 

- The code must be formatted with [Black](https://github.com/psf/black), here is the command to use to reformat your code : 
```bash
black . -l 79
```
- CI must be green on Travis