|
| 1 | +# Creating and deploying custom code |
| 2 | + |
| 3 | +In some cases, you may need to create and deploy custom code as part of your MLOps workflow using vetiver. This could be necessary when you need to: |
| 4 | + |
| 5 | +- deploy custom models in vetiver |
| 6 | +- deploy unsupported models in vetiver |
| 7 | +- include custom code in vetiver |
| 8 | +- deploy a vetiver model with a custom pipeline |
| 9 | + |
| 10 | + You may also have custom code in a known framework, such as a column transformer for a scikit-learn model. |
| 11 | + |
| 12 | +In these cases, extra steps will be required to successfully create and deploy a `VetiverModel` object. |
| 13 | + |
| 14 | +# Making a custom model |
| 15 | + |
| 16 | +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 new model handler. |
| 17 | + |
| 18 | +To create a model handler, you should create a subclass of vetiver's `BaseHandler` class. This handler should include the following: |
| 19 | + |
| 20 | +- `model_type`: A static method that declares the type of your model. |
| 21 | +- `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. |
| 22 | + |
| 23 | +Here's an example of a handler for a model of `newmodeltype` type. Once you have defined your handler, you can initialize it with your model and pass it to the `VetiverModel` class. |
| 24 | + |
| 25 | +```python |
| 26 | +from vetiver.handlers.base import BaseHandler |
| 27 | + |
| 28 | +class CustomHandler(BaseHandler): |
| 29 | + def __init__(self, model, prototype_data): |
| 30 | + super().__init__(model, prototype_data) |
| 31 | + |
| 32 | + model_type = staticmethod(lambda: newmodeltype) |
| 33 | + pip_name = "scikit-learn" # package's installation name on pip |
| 34 | + |
| 35 | + def handler_predict(self, input_data, check_prototype: bool): |
| 36 | + """ |
| 37 | + Your code for making predictions using the custom model |
| 38 | +
|
| 39 | + Parameters |
| 40 | + ---------- |
| 41 | + input_data: |
| 42 | + Data POSTed to API endpoint |
| 43 | + check_prototype: bool |
| 44 | + Whether the prototype should be enforced |
| 45 | + """ |
| 46 | + prediction = model.fancy_new_predict(input_data) |
| 47 | + |
| 48 | + return prediction |
| 49 | + |
| 50 | +new_model = CustomHandler(model, prototype_data) |
| 51 | + |
| 52 | +VetiverModel(new_model, "custom_model") |
| 53 | +``` |
| 54 | + |
| 55 | +If your model is a common type, please consider [submitting a pull request](https://github.com/rstudio/vetiver-python/pulls). |
| 56 | + |
| 57 | +To deploy custom code, 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. |
| 58 | + |
| 59 | +# Deploying custom elements |
| 60 | + |
| 61 | +If your `VetiverModel` includes custom source code, you need to include that code in your deployment files to build an API in another location. The example below shows a user-created `FeatureSelector`, which is part of a scikit-learn pipeline. |
| 62 | + |
| 63 | +```{.python filename="model.py"} |
| 64 | +from sklearn.base import BaseEstimator, TransformerMixin |
| 65 | +from sklearn.tree import DecisionTreeClassifier |
| 66 | +from sklearn.pipeline import Pipeline |
| 67 | +
|
| 68 | +# create custom data preprocessing |
| 69 | +class FeatureSelector(BaseEstimator, TransformerMixin): |
| 70 | + def __init__(self, columns): |
| 71 | + self.columns = columns |
| 72 | +
|
| 73 | + def fit(self, X, y=None): |
| 74 | + return self |
| 75 | +
|
| 76 | + def transform(self, X, y=None): |
| 77 | + return X[self.columns] |
| 78 | +
|
| 79 | +# create model |
| 80 | +model = Pipeline(steps=[ |
| 81 | + ('feature_selector', FeatureSelector(features)), |
| 82 | + ('decision_tree', DecisionTreeClassifier()) |
| 83 | +]) |
| 84 | +
|
| 85 | +# create deployable model object |
| 86 | +from vetiver import VetiverModel, vetiver_pin_write |
| 87 | +
|
| 88 | +v = VetiverModel(model, "selected_decision_tree", protoype_data = X) |
| 89 | +
|
| 90 | +# pin model to some location, eg, Posit Connect |
| 91 | +import pins |
| 92 | +
|
| 93 | +board = pins.board_connect(allow_pickle_read=True) |
| 94 | +vetiver_pin_write(board, v) |
| 95 | +``` |
| 96 | + |
| 97 | +::: {.panel-tabset} |
| 98 | +## Docker |
| 99 | + |
| 100 | +To generate files needed to start a Docker container, you can use the command `vetiver.prepare_docker`. |
| 101 | + |
| 102 | +```{.python} |
| 103 | +vetiver.prepare_docker(board, "selected_decision_tree") |
| 104 | +``` |
| 105 | + |
| 106 | +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}`. |
| 107 | + |
| 108 | +```{.python filename="app.py"} |
| 109 | +from vetiver import VetiverModel |
| 110 | +import vetiver |
| 111 | +import pins |
| 112 | +from model import FeatureSelector # add this line to import your custom feature engineering |
| 113 | +
|
| 114 | +
|
| 115 | +b = pins.board_connect(allow_pickle_read=True) |
| 116 | +v = VetiverModel.from_pin(b, 'selected_decision_tree') |
| 117 | +
|
| 118 | +vetiver_api = vetiver.VetiverAPI(v) |
| 119 | +api = vetiver_api.app |
| 120 | +``` |
| 121 | + |
| 122 | +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. |
| 123 | + |
| 124 | +```{.bash filename="Dockerfile"} |
| 125 | +# # Generated by the vetiver package; edit with care |
| 126 | +# start with python base image |
| 127 | +FROM python:3.10 |
| 128 | + |
| 129 | +# create directory in container for vetiver files |
| 130 | +WORKDIR /vetiver |
| 131 | + |
| 132 | +# copy and install requirements |
| 133 | +COPY vetiver_requirements.txt /vetiver/requirements.txt |
| 134 | + |
| 135 | +# |
| 136 | +RUN pip install --no-cache-dir --upgrade -r /vetiver/requirements.txt |
| 137 | + |
| 138 | +# copy app file |
| 139 | +COPY app.py /vetiver/app/app.py |
| 140 | + |
| 141 | +# ADD THIS LINE to copy model source code |
| 142 | +COPY model.py /vetiver/app/model.py |
| 143 | + |
| 144 | +# expose port |
| 145 | +EXPOSE 8080 |
| 146 | + |
| 147 | +# run vetiver API |
| 148 | +CMD ["uvicorn", "app.app:api", "--host", "0.0.0.0", "--port", "8080"] |
| 149 | +``` |
| 150 | + |
| 151 | +## Posit Connect |
| 152 | + |
| 153 | +To deploy custom code to Posit Connect, you'll first start with the command `vetiver.write_app`. |
| 154 | + |
| 155 | +```{.python} |
| 156 | +vetiver.write_app(board, 'selected_decision_tree') |
| 157 | +``` |
| 158 | + |
| 159 | +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}`. |
| 160 | + |
| 161 | +```{.python filename=="app.py"} |
| 162 | +from vetiver import VetiverModel |
| 163 | +import vetiver |
| 164 | +import pins |
| 165 | +from model import FeatureSelector # add this line to import your custom feature engineering |
| 166 | +
|
| 167 | +
|
| 168 | +b = pins.board_connect(allow_pickle_read=True) |
| 169 | +v = VetiverModel.from_pin(b, 'selected_decision_tree') |
| 170 | +
|
| 171 | +vetiver_api = vetiver.VetiverAPI(v) |
| 172 | +api = vetiver_api.app |
| 173 | +``` |
| 174 | + |
| 175 | +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. |
| 176 | + |
| 177 | +```{.python} |
| 178 | +from rsconnect.api.actions import deploy_python_fastapi |
| 179 | +import rsconnect |
| 180 | +
|
| 181 | +url = "example.connect.com" # your Posit Connect server url |
| 182 | +api_key = os.environ(CONNECT_API_KEY) # your Posit Connect API key |
| 183 | +
|
| 184 | +connect_server = rsconnect.api.RSConnectServer( |
| 185 | + url = url, |
| 186 | + api_key = api_key |
| 187 | +) |
| 188 | +
|
| 189 | +rsconnect.actions.deploy_python_fastapi( |
| 190 | + connect_server = connect_server, |
| 191 | + directory = "./", # path to the directory containing the app.py and model.py files |
| 192 | + entry_point = "app:api" # the API is the app.py file, in a variable named api |
| 193 | + ) |
| 194 | +
|
| 195 | +``` |
| 196 | + |
| 197 | +::: |
| 198 | + |
| 199 | +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). |
0 commit comments