|
25 | 25 | from feast.on_demand_feature_view import on_demand_feature_view |
26 | 26 | from feast.types import Array, Float32, Int64, String |
27 | 27 |
|
| 28 | +try: |
| 29 | + # Import static artifacts helpers (available when feature server loads artifacts) |
| 30 | + from static_artifacts import get_sentiment_model, get_lookup_tables |
| 31 | +except ImportError: |
| 32 | + # Fallback for when static_artifacts.py is not available |
| 33 | + get_sentiment_model = None |
| 34 | + get_lookup_tables = None |
| 35 | + |
| 36 | +# Global references for static artifacts (set by feature server) |
| 37 | +_sentiment_model = None |
| 38 | +_lookup_tables = {} |
| 39 | + |
28 | 40 | # Configuration |
29 | 41 | repo_path = Path(__file__).parent |
30 | 42 | data_path = repo_path / "data" |
|
143 | 155 | ) |
144 | 156 | def sentiment_prediction(inputs: pd.DataFrame) -> pd.DataFrame: |
145 | 157 | """ |
146 | | - Real-time sentiment prediction using pre-trained models. |
| 158 | + Real-time sentiment prediction using pre-loaded static artifacts. |
147 | 159 |
|
148 | | - This function demonstrates how to integrate PyTorch/HuggingFace models |
149 | | - directly into Feast feature views for real-time inference. |
| 160 | + This function demonstrates how to use static artifacts (pre-loaded models, |
| 161 | + lookup tables) for efficient real-time inference. Models are loaded once |
| 162 | + at feature server startup rather than on each request. |
150 | 163 | """ |
151 | 164 | try: |
152 | 165 | import numpy as np |
153 | | - from transformers import pipeline |
154 | 166 | except ImportError: |
155 | | - # Fallback to dummy predictions if dependencies aren't available |
156 | | - df = pd.DataFrame() |
157 | | - df["predicted_sentiment"] = ["neutral"] * len(inputs) |
158 | | - df["sentiment_confidence"] = np.array([0.5] * len(inputs), dtype=np.float32) |
159 | | - df["positive_prob"] = np.array([0.33] * len(inputs), dtype=np.float32) |
160 | | - df["negative_prob"] = np.array([0.33] * len(inputs), dtype=np.float32) |
161 | | - df["neutral_prob"] = np.array([0.34] * len(inputs), dtype=np.float32) |
162 | | - df["text_embedding"] = [[np.float32(0.0)] * 384] * len(inputs) |
163 | | - return df |
| 167 | + # Fallback to dummy predictions if numpy isn't available |
| 168 | + import array as np_fallback |
164 | 169 |
|
165 | | - # Initialize model (in production, you'd want to cache this) |
166 | | - model_name = "cardiffnlp/twitter-roberta-base-sentiment-latest" |
167 | | - try: |
168 | | - # Use sentiment pipeline for convenience (force CPU to avoid MPS forking issues) |
169 | | - sentiment_pipeline = pipeline( |
170 | | - "sentiment-analysis", |
171 | | - model=model_name, |
172 | | - tokenizer=model_name, |
173 | | - return_all_scores=True, |
174 | | - device="cpu", # Force CPU to avoid MPS forking issues on macOS |
175 | | - ) |
176 | | - |
177 | | - except Exception: |
178 | | - # Fallback if model loading fails |
179 | 170 | df = pd.DataFrame() |
180 | 171 | df["predicted_sentiment"] = ["neutral"] * len(inputs) |
181 | | - df["sentiment_confidence"] = np.array([0.5] * len(inputs), dtype=np.float32) |
182 | | - df["positive_prob"] = np.array([0.33] * len(inputs), dtype=np.float32) |
183 | | - df["negative_prob"] = np.array([0.33] * len(inputs), dtype=np.float32) |
184 | | - df["neutral_prob"] = np.array([0.34] * len(inputs), dtype=np.float32) |
185 | | - df["text_embedding"] = [[np.float32(0.0)] * 384] * len(inputs) |
| 172 | + df["sentiment_confidence"] = [0.5] * len(inputs) |
| 173 | + df["positive_prob"] = [0.33] * len(inputs) |
| 174 | + df["negative_prob"] = [0.33] * len(inputs) |
| 175 | + df["neutral_prob"] = [0.34] * len(inputs) |
| 176 | + df["text_embedding"] = [[0.0] * 384] * len(inputs) |
186 | 177 | return df |
187 | 178 |
|
| 179 | + # Get pre-loaded static artifacts from global references |
| 180 | + # These are loaded once at startup via static_artifacts.py |
| 181 | + global _sentiment_model, _lookup_tables |
| 182 | + |
| 183 | + sentiment_model = _sentiment_model |
| 184 | + lookup_tables = _lookup_tables |
| 185 | + |
| 186 | + # Use lookup table for label mapping (from static artifacts) |
| 187 | + label_map = lookup_tables.get("sentiment_labels", { |
| 188 | + "LABEL_0": "negative", |
| 189 | + "LABEL_1": "neutral", |
| 190 | + "LABEL_2": "positive" |
| 191 | + }) |
| 192 | + |
188 | 193 | results = [] |
189 | 194 |
|
190 | 195 | for text in inputs["input_text"]: |
191 | 196 | try: |
192 | | - # Get sentiment predictions |
193 | | - predictions = sentiment_pipeline(text) |
194 | | - |
195 | | - # Parse results (RoBERTa model returns LABEL_0, LABEL_1, LABEL_2) |
196 | | - label_map = { |
197 | | - "LABEL_0": "negative", |
198 | | - "LABEL_1": "neutral", |
199 | | - "LABEL_2": "positive", |
200 | | - } |
201 | | - |
202 | | - scores = { |
203 | | - label_map.get(pred["label"], pred["label"]): pred["score"] |
204 | | - for pred in predictions |
205 | | - } |
206 | | - |
207 | | - # Get best prediction |
208 | | - best_pred = max(predictions, key=lambda x: x["score"]) |
209 | | - predicted_sentiment = label_map.get(best_pred["label"], best_pred["label"]) |
210 | | - confidence = best_pred["score"] |
211 | | - |
212 | | - # Get embeddings (simplified - dummy embeddings for demo) |
213 | | - # In a real implementation, you'd run the model to get embeddings |
214 | | - # For this demo, we'll create a dummy embedding |
215 | | - embedding = np.random.rand(384).tolist() # DistilBERT size |
216 | | - |
217 | | - results.append( |
218 | | - { |
219 | | - "predicted_sentiment": predicted_sentiment, |
220 | | - "sentiment_confidence": np.float32(confidence), |
221 | | - "positive_prob": np.float32(scores.get("positive", 0.0)), |
222 | | - "negative_prob": np.float32(scores.get("negative", 0.0)), |
223 | | - "neutral_prob": np.float32(scores.get("neutral", 0.0)), |
224 | | - "text_embedding": [np.float32(x) for x in embedding], |
| 197 | + if sentiment_model is not None: |
| 198 | + # Use pre-loaded model for prediction |
| 199 | + predictions = sentiment_model(text) |
| 200 | + |
| 201 | + # Parse results using static lookup tables |
| 202 | + scores = { |
| 203 | + label_map.get(pred["label"], pred["label"]): pred["score"] |
| 204 | + for pred in predictions |
225 | 205 | } |
226 | | - ) |
| 206 | + |
| 207 | + # Get best prediction |
| 208 | + best_pred = max(predictions, key=lambda x: x["score"]) |
| 209 | + predicted_sentiment = label_map.get(best_pred["label"], best_pred["label"]) |
| 210 | + confidence = best_pred["score"] |
| 211 | + else: |
| 212 | + # Fallback when model is not available |
| 213 | + predicted_sentiment = "neutral" |
| 214 | + confidence = 0.5 |
| 215 | + scores = {"positive": 0.33, "negative": 0.33, "neutral": 0.34} |
| 216 | + |
| 217 | + # Generate dummy embeddings (in production, use pre-loaded embeddings) |
| 218 | + embedding = np.random.rand(384).tolist() |
| 219 | + |
| 220 | + results.append({ |
| 221 | + "predicted_sentiment": predicted_sentiment, |
| 222 | + "sentiment_confidence": np.float32(confidence), |
| 223 | + "positive_prob": np.float32(scores.get("positive", 0.0)), |
| 224 | + "negative_prob": np.float32(scores.get("negative", 0.0)), |
| 225 | + "neutral_prob": np.float32(scores.get("neutral", 0.0)), |
| 226 | + "text_embedding": [np.float32(x) for x in embedding], |
| 227 | + }) |
227 | 228 |
|
228 | 229 | except Exception: |
229 | 230 | # Fallback for individual text processing errors |
230 | | - results.append( |
231 | | - { |
232 | | - "predicted_sentiment": "neutral", |
233 | | - "sentiment_confidence": np.float32(0.5), |
234 | | - "positive_prob": np.float32(0.33), |
235 | | - "negative_prob": np.float32(0.33), |
236 | | - "neutral_prob": np.float32(0.34), |
237 | | - "text_embedding": [np.float32(0.0)] * 384, |
238 | | - } |
239 | | - ) |
| 231 | + results.append({ |
| 232 | + "predicted_sentiment": "neutral", |
| 233 | + "sentiment_confidence": np.float32(0.5), |
| 234 | + "positive_prob": np.float32(0.33), |
| 235 | + "negative_prob": np.float32(0.33), |
| 236 | + "neutral_prob": np.float32(0.34), |
| 237 | + "text_embedding": [np.float32(0.0)] * 384, |
| 238 | + }) |
240 | 239 |
|
241 | 240 | return pd.DataFrame(results) |
242 | 241 |
|
|
0 commit comments