Skip to content

Commit

Permalink
docs on deploying custom elements
Browse files Browse the repository at this point in the history
  • Loading branch information
isabelizimm committed Oct 13, 2023
1 parent 5639bf6 commit be8bc6b
Show file tree
Hide file tree
Showing 2 changed files with 194 additions and 46 deletions.
194 changes: 194 additions & 0 deletions docs/custom_elements.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
# Creating and deploying custom elements

In some cases, you may need to create and deploy custom elements as part of your MLOps workflow using Vetiver. This could be necessary when there is no existing implementation for the type of model you want to deploy, or when you want to implement a current handler in a different way. You may also have a custom element in a known framework, such as a custom column transformer for a scikit-learn model.

In these cases, extra steps will be required to successfully create and deploy a `VetiverModel` object.

# Making a custom model

Vetiver supports basic [scikit-learn](https://scikit-learn.org/), [torch](https://pytorch.org/), [statsmodels](https://www.statsmodels.org/stable/index.html), [xgboost](https://xgboost.readthedocs.io/en/stable/), and [spacy](https://spacy.io/) models. If you need to alter the usage of these models, or deploy a different type of model, you will likely need to create a custom model handler.

To create a custom model handler, you should create a subclass of Vetiver's BaseHandler class. This custom handler should include the following:

- `model_type`: A static method that declares the type of your model.
- `handler_predict()`: A method that defines how predictions should be made for your model. This method is used at the /predict endpoint in the VetiverAPI.

Here's an example of a custom handler for a model of `newmodeltype` type.

```python
from vetiver.handlers.base import BaseHandler

class CustomHandler(BaseHandler):
def __init__(self, model, ptype_data):
super().__init__(model, ptype_data)

model_type = staticmethod(lambda: newmodeltype)
pip_name = "scikit-learn" # package's installation name on pip

def handler_predict(self, input_data, check_prototype: bool):
"""
Your code for making predictions using the custom model
Parameters
----------
input_data:
Data POSTed to API endpoint
check_prototype: bool
Whether the prototype should be enforced
"""
prediction = model.fancy_new_predict(input_data)

return prediction

new_model = CustomHandler(model, ptype_data)

VetiverModel(new_model, "custom_model")
```

Once you have defined your custom handler, you can initialize it with your model and pass it to the VetiverModel class.

If your model is a common type, please consider [submitting a pull request](https://github.com/rstudio/vetiver-python/pulls).

To deploy custom elements, you need to include the necessary source code in your deployment files. If your model or other elements can be imported from a Python package, you can include the relevant packages in a `requirements.txt` file for deployment. However, if you have custom source code in local files, you will need to include those files in the deployment process.

# Deploying custom elements

If your `VetiverModel` includes custom source code, you need to include that code in your deployment files to build an API in another location. To do so,

```{.python filename="model.py"}
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.tree import DecisionTreeClassifier
from sklearn.pipeline import Pipeline
# create custom data preprocessing
class FeatureSelector(BaseEstimator, TransformerMixin):
def __init__(self, columns):
self.columns = columns
def fit(self, X, y=None):
return self
def transform(self, X, y=None):
return X[self.columns]
# create model
model = Pipeline(steps=[
('feature_selector', FeatureSelector(features)),
('decision_tree', DecisionTreeClassifier())
])
# create deployable model object
from vetiver import VetiverModel, vetiver_pin_write
v = VetiverModel(model, "selected_decision_tree", protoype_data = X)
# pin model to some location, eg, Posit Connect
import pins
board = pins.board_connect(allow_pickle_read=True)
vetiver_pin_write(board, v)
```

::: {.panel-tabset}
## Docker

To generate files needed to start a Docker container, you can use the command `vetiver.prepare_docker`.

```{.python}
vetiver.prepare_docker(board, "selected_decision_tree")
```

When you run this line, 3 files are generated: a Dockerfile, an `app.py` file, and a `vetiver_requirements.txt`. In the `app.py` file, you'll need to add an import statement that is formatted `from {name of file, excluding .py, that has custom element} import {name of custom element}`.

```{.python filename=="app.py"}
from vetiver import VetiverModel
import vetiver
import pins
from model import FeatureSelector # add this line to import your custom feature engineering
b = pins.board_connect(allow_pickle_read=True)
v = VetiverModel.from_pin(b, 'selected_decision_tree')
vetiver_api = vetiver.VetiverAPI(v)
api = vetiver_api.app
```

Add a line to your Dockerfile to copy your source file(s) into your Docker container. The format will be `COPY path/to/your/filename.py /vetiver/app/filename.py`, where the destination is always in the `/vetiver/app/` directory.

```{.bash filename="Dockerfile"}
# # Generated by the vetiver package; edit with care
# start with python base image
FROM python:3.10

# create directory in container for vetiver files
WORKDIR /vetiver

# copy and install requirements
COPY vetiver_requirements.txt /vetiver/requirements.txt

#
RUN pip install --no-cache-dir --upgrade -r /vetiver/requirements.txt

# copy app file
COPY app.py /vetiver/app/app.py

# ADD THIS LINE to copy model source code
COPY model.py /vetiver/app/model.py

# expose port
EXPOSE 8080

# run vetiver API
CMD ["uvicorn", "app.app:api", "--host", "0.0.0.0", "--port", "8080"]
```

## Posit Connect

To deploy custom elements to Posit Connect, you'll first start with the command `vetiver.write_app`.

```{.python}
vetiver.write_app(board, 'selected_decision_tree')
```

This will generate an `app.py` file, where you'll need to add an import statement that is formatted `from {name of file, excluding .py, that has custom element} import {name of custom element}`.

```{.python filename=="app.py"}
from vetiver import VetiverModel
import vetiver
import pins
from model import FeatureSelector # add this line to import your custom feature engineering
b = pins.board_connect(allow_pickle_read=True)
v = VetiverModel.from_pin(b, 'selected_decision_tree')
vetiver_api = vetiver.VetiverAPI(v)
api = vetiver_api.app
```

After editing the app.py file, you can deploy it to Posit Connect using the `rsconnect` package. Use the `rsconnect.api.actions.deploy_python_fastapi()` function to deploy the API, specifying the Connect server URL, API key, directory containing the `app.py` and `model.py` files, and the entry point of the API.

```{.python}
from rsconnect.api.actions import deploy_python_fastapi
import rsconnect
url = "example.connect.com" # your Posit Connect server url
api_key = os.environ(CONNECT_API_KEY) # your Posit Connect API key
connect_server = rsconnect.api.RSConnectServer(
url = url
api_key = api_key
)
rsconnect.actions.deploy_python_fastapi(
connect_server = connect_server,
directory = "./", # path to the directory containing the app.py and model.py files
entry_point = "app:api" # the API is the app.py file, in a variable named api
)
```

:::

Please note that the above steps are a general guide, and you may need to adapt them to your specific use case and deployment environment. If you have any questions, please consider [opening an issue](https://github.com/rstudio/vetiver-python/issues/new).
46 changes: 0 additions & 46 deletions docs/custom_handler.md

This file was deleted.

0 comments on commit be8bc6b

Please sign in to comment.