orient_express 0.2.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ from .model_wrapper import ModelExpress
@@ -0,0 +1,197 @@
1
+ import os
2
+ import joblib
3
+ import logging
4
+ import pandas as pd
5
+ from google.cloud import storage
6
+ from google.cloud import aiplatform
7
+
8
+
9
+ class ModelExpress:
10
+ def __init__(
11
+ self,
12
+ model_name,
13
+ project_name,
14
+ bucket_name,
15
+ model_version=None,
16
+ model=None,
17
+ region="us-central1",
18
+ serialized_model_path="model.joblib",
19
+ docker_image_uri="us-docker.pkg.dev/vertex-ai/prediction/xgboost-cpu.1-7:latest",
20
+ endpoint_name=None,
21
+ machine_type="n1-standard-4",
22
+ min_replica_count=1,
23
+ max_replica_count=1,
24
+ ):
25
+ self.model = model
26
+ self.model_name = model_name
27
+ self.model_version = model_version
28
+ self.region = region
29
+ self.project_name = project_name
30
+ self.bucket_name = bucket_name
31
+ self.serialized_model_path = serialized_model_path
32
+ self.docker_image_uri = docker_image_uri
33
+ self.machine_type = machine_type
34
+ self.min_replica_count = min_replica_count
35
+ self.max_replica_count = max_replica_count
36
+ self.endpoint = None
37
+
38
+ if not endpoint_name:
39
+ self.endpoint_name = f"model-xpress-{model_name}"
40
+ else:
41
+ self.endpoint_name = endpoint_name
42
+
43
+ def colab_auth(self):
44
+ from google.colab import auth
45
+
46
+ auth.authenticate_user()
47
+
48
+ def _vertex_init(self):
49
+ aiplatform.init(project=self.project_name, location=self.region)
50
+
51
+ def get_latest_vertex_model(self, model_name):
52
+ self._vertex_init()
53
+
54
+ # Search for models with the specified display name
55
+ models = aiplatform.Model.list(filter=f"display_name={model_name}")
56
+
57
+ if not models:
58
+ return None # Return None if no model with the given name exists
59
+
60
+ # Sort models by update time in descending order to get the latest version
61
+ latest_model = sorted(models, key=lambda x: x.update_time, reverse=True)[0]
62
+ return latest_model
63
+
64
+ # upload model to vertex ai model registry
65
+ def upload(self):
66
+ joblib.dump(self.model, self.serialized_model_path)
67
+ logging.info(f"Model saved to {self.serialized_model_path}")
68
+
69
+ # Initialize the GCS client
70
+ client = storage.Client()
71
+ bucket = client.bucket(self.bucket_name)
72
+
73
+ last_model = self.get_latest_vertex_model(self.model_name)
74
+ if last_model:
75
+ last_version = last_model.version_id
76
+ new_version = int(last_version) + 1
77
+ else:
78
+ new_version = 1
79
+
80
+ # Upload the model file
81
+ blob = bucket.blob(
82
+ self.get_artifacts_path(new_version, self.serialized_model_path)
83
+ )
84
+ blob.upload_from_filename(self.serialized_model_path)
85
+
86
+ return self.create_model_version(new_version, last_model)
87
+
88
+ def get_artifacts_path(self, version, file_name=None):
89
+ dir_name = f"models/{self.model_name}/{version}"
90
+ if file_name:
91
+ return f"{dir_name}/{file_name}"
92
+
93
+ return f"{dir_name}/"
94
+
95
+ def create_model_version(self, version_number, last_version):
96
+ artifact_uri = (
97
+ f"gs://{self.bucket_name}/{self.get_artifacts_path(version_number)}"
98
+ )
99
+
100
+ if last_version:
101
+ parent_model = f"projects/{self.project_name}/locations/{self.region}/models/{last_version.name}"
102
+ else:
103
+ parent_model = None
104
+
105
+ release = aiplatform.Model.upload(
106
+ display_name=self.model_name,
107
+ artifact_uri=artifact_uri,
108
+ parent_model=parent_model,
109
+ serving_container_image_uri=self.docker_image_uri,
110
+ sync=True,
111
+ )
112
+
113
+ return release
114
+
115
+ def get_or_create_endpoint(self):
116
+ endpoint = self.get_endpoint()
117
+ if endpoint:
118
+ return endpoint
119
+ else:
120
+ return self.create_endpoint()
121
+
122
+ def get_endpoint(self):
123
+ endpoints = aiplatform.Endpoint.list(
124
+ filter=f"display_name={self.endpoint_name}", order_by="create_time"
125
+ )
126
+ if endpoints:
127
+ return endpoints[0]
128
+
129
+ def create_endpoint(self):
130
+ return aiplatform.Endpoint.create(
131
+ display_name=self.endpoint_name,
132
+ project=self.project_name,
133
+ location=self.region,
134
+ )
135
+
136
+ def deploy(self):
137
+ self._vertex_init()
138
+
139
+ endpoint = self.get_or_create_endpoint()
140
+ model_version = self.upload()
141
+ model_version.deploy(
142
+ endpoint=endpoint,
143
+ machine_type=self.machine_type,
144
+ min_replica_count=self.min_replica_count,
145
+ max_replica_count=self.max_replica_count,
146
+ traffic_percentage=100,
147
+ )
148
+
149
+ def remote_predict(self, input_df):
150
+ if not self.endpoint:
151
+ endpoint = self.get_endpoint()
152
+ if not endpoint:
153
+ raise Exception(
154
+ f"Endpoint '{self.endpoint_name}' not found. Please deploy the model first."
155
+ )
156
+ self.endpoint = endpoint
157
+
158
+ instances = self.df_to_features(input_df)
159
+ predictions = self.endpoint.predict(instances=instances)
160
+ return predictions.predictions
161
+
162
+ def local_predict(self, input_df):
163
+ if not self.model:
164
+ self.load_model_from_registry()
165
+
166
+ return self.model.predict(input_df)
167
+
168
+ def load_model_from_registry(self):
169
+ if self.model_version:
170
+ vertex_model = aiplatform.Model(
171
+ model_name=self.model_name, version=self.model_version
172
+ )
173
+
174
+ else:
175
+ vertex_model = self.get_latest_vertex_model(self.model_name)
176
+
177
+ if not vertex_model:
178
+ raise Exception(f"Model '{self.model_name}' not found in the registry.")
179
+
180
+ artifact_uri = vertex_model.gca_resource.artifact_uri
181
+ self.download_artifacts(artifact_uri)
182
+
183
+ self.model = joblib.load(self.serialized_model_path)
184
+
185
+ def download_artifacts(self, artifact_uri):
186
+ storage_client = storage.Client()
187
+ bucket_name, artifact_path = artifact_uri.replace("gs://", "").split("/", 1)
188
+ bucket = storage_client.bucket(bucket_name)
189
+
190
+ blobs = bucket.list_blobs(prefix=artifact_path)
191
+ for blob in blobs:
192
+ artifact_path = blob.name.split("/")[-1]
193
+ blob.download_to_filename(artifact_path)
194
+
195
+ def df_to_features(self, df: pd.DataFrame):
196
+ #
197
+ return df.to_dict(orient="records")
@@ -0,0 +1,175 @@
1
+ Metadata-Version: 2.1
2
+ Name: orient_express
3
+ Version: 0.2.2
4
+ Summary: A library to simplify model deployment to Vertex AI
5
+ Author: Alexey Zankevich
6
+ Author-email: alex.zankevich@shiftsmart.com
7
+ Requires-Python: >=3.9
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.9
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Requires-Dist: google-cloud-aiplatform
14
+ Requires-Dist: google-cloud-storage
15
+ Requires-Dist: pandas
16
+ Requires-Dist: scikit-learn (>=1.5.2,<2.0.0)
17
+ Requires-Dist: xgboost (>=2.1.2,<3.0.0)
18
+ Description-Content-Type: text/markdown
19
+
20
+ # Orient Express
21
+ A library to accelerate model deployments to Vertex AI directly from colab notebooks
22
+
23
+ ![train-resized](https://github.com/user-attachments/assets/f1ed32ec-07d9-4d48-8b96-3323db6b5091)
24
+
25
+ ## Installation
26
+
27
+ ```
28
+ pip install orient_express
29
+ ```
30
+
31
+ ## Example
32
+
33
+ ### Train Model
34
+
35
+ Train a regular model. In the example below, it's xgboost model, trained on the Titanic dataset.
36
+
37
+ ```python
38
+
39
+ # Import necessary libraries
40
+ import xgboost as xgb
41
+ from sklearn.model_selection import train_test_split
42
+ from sklearn.metrics import accuracy_score, confusion_matrix
43
+ from sklearn.pipeline import Pipeline
44
+ from sklearn.compose import ColumnTransformer
45
+ from sklearn.preprocessing import StandardScaler, OneHotEncoder
46
+ from sklearn.impute import SimpleImputer
47
+
48
+ # Load the Titanic dataset
49
+ data = sns.load_dataset('titanic').dropna(subset=['survived']) # Dropping rows with missing target labels
50
+
51
+ # Select features and target
52
+ X = data[['pclass', 'sex', 'age', 'sibsp', 'parch', 'fare', 'embarked']]
53
+ y = data['survived']
54
+
55
+ # Define preprocessing for numeric columns (impute missing values and scale features)
56
+ numeric_features = ['age', 'fare', 'sibsp', 'parch']
57
+ numeric_transformer = Pipeline(steps=[
58
+ ('imputer', SimpleImputer(strategy='median')),
59
+ ('scaler', StandardScaler())
60
+ ])
61
+
62
+ # Define preprocessing for categorical columns (impute missing values and one-hot encode)
63
+ categorical_features = ['pclass', 'sex', 'embarked']
64
+ categorical_transformer = Pipeline(steps=[
65
+ ('imputer', SimpleImputer(strategy='most_frequent')),
66
+ ('onehot', OneHotEncoder(handle_unknown='ignore'))
67
+ ])
68
+
69
+ # Combine preprocessing steps
70
+ preprocessor = ColumnTransformer(
71
+ transformers=[
72
+ ('num', numeric_transformer, numeric_features),
73
+ ('cat', categorical_transformer, categorical_features)
74
+ ])
75
+
76
+ # Create a pipeline that first transforms the data, then trains an XGBoost model
77
+ model = Pipeline(steps=[
78
+ ('preprocessor', preprocessor),
79
+ ('classifier', xgb.XGBClassifier(use_label_encoder=False, eval_metric='logloss'))
80
+ ])
81
+
82
+ # Split the dataset into training and test sets
83
+ X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
84
+
85
+ # Train the model
86
+ model.fit(X_train, y_train)
87
+ ```
88
+
89
+ ## Upload Model To Model Registry
90
+
91
+ ```python
92
+
93
+ model_wrapper = ModelExpress(model=model,
94
+ project_name='my-project-name',
95
+ region='us-central1',
96
+ bucket_name='my-artifacts-bucket',
97
+ model_name='titanic')
98
+ model_wrapper.upload()
99
+ ```
100
+
101
+ ## Local Inference (Without Online Prediction Endpoint)
102
+
103
+ The following code will download the last model from the model registry and run the inference locally.
104
+
105
+ ```python
106
+
107
+ # create input dataframe
108
+ titanic_data = {
109
+ "pclass": [1], # Passenger class (1st, 2nd, 3rd)
110
+ "sex": ["female"], # Gender
111
+ "age": [29], # Age
112
+ "sibsp": [0], # Number of siblings/spouses aboard
113
+ "parch": [0], # Number of parents/children aboard
114
+ "fare": [100.0], # Ticket fare
115
+ "embarked": ["S"] # Port of Embarkation (C = Cherbourg, Q = Queenstown, S = Southampton)
116
+ }
117
+ input_df = pd.DataFrame(titanic_data)
118
+
119
+ # init the model wrapper
120
+ model_wrapper = ModelExpress(project_name='my-project-name',
121
+ region='us-central1',
122
+ model_name='titanic')
123
+
124
+ # Run inference locally
125
+ # It will download the most recent version from the model registry automatically
126
+ model_wrapper.local_predict(input_df)
127
+ ```
128
+
129
+ ## Pin Model Version
130
+
131
+ In many cases, the pipeline should be pinned to a specific model version so the model can only
132
+ be updated explicitly. Just pass a `model_version` parameter when instantiating the ModelExpress wrapper.
133
+
134
+ ```python
135
+
136
+ # init the model wrapper
137
+ model_wrapper = ModelExpress(project_name='my-project-name',
138
+ region='us-central1',
139
+ model_name='titanic',
140
+ model_version=11)
141
+ ```
142
+
143
+ ## Remote Inference (With Online Prediction Endpoint)
144
+
145
+ Make sure the model is deployed:
146
+ ```python
147
+
148
+ model_wrapper = ModelExpress(model=model,
149
+ project_name='my-project-name',
150
+ region='us-central1',
151
+ bucket_name='my-artifacts-bucket',
152
+ model_name='titanic')
153
+
154
+ # upload the version to the registry and deploy it to the endpoint
155
+ model_wrapper.deploy()
156
+ ```
157
+
158
+ Run inference with `remote_predict` method. It will make a remote call to the endpoint without fetching the model locally.
159
+
160
+ ```python
161
+
162
+ titanic_data = {
163
+ "pclass": [1], # Passenger class (1st, 2nd, 3rd)
164
+ "sex": ["female"], # Gender
165
+ "age": [29], # Age
166
+ "sibsp": [0], # Number of siblings/spouses aboard
167
+ "parch": [0], # Number of parents/children aboard
168
+ "fare": [100.0], # Ticket fare
169
+ "embarked": ["S"] # Port of Embarkation (C = Cherbourg, Q = Queenstown, S = Southampton)
170
+ }
171
+ df = pd.DataFrame(titanic_data)
172
+
173
+ model_wrapper.remote_predict(df)
174
+ ```
175
+
@@ -0,0 +1,5 @@
1
+ orient_express/__init__.py,sha256=I7VRbcJKqePRPtOiZl3ro9gwwGWiBcj-DvaWPHIEUqQ,40
2
+ orient_express/model_wrapper.py,sha256=FnIq4ExP-rIpik0LrhlMfOyszzbeNQLRPni8dsD640M,6385
3
+ orient_express-0.2.2.dist-info/METADATA,sha256=e1bbcbfIprg4xA-8fXAmDMpmkrr3rVdpbeS98aQNqFY,5821
4
+ orient_express-0.2.2.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
5
+ orient_express-0.2.2.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 1.9.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any