File size: 7,982 Bytes
930c998 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 | import pandas as pd
import numpy as np
from typing import Dict, Any
import os
import pickle
import re
from gensim.models.doc2vec import Doc2Vec
import json
class EndpointHandler:
"""
Custom handler for Hugging Face Inference Endpoints.
Handles Doc2Vec + Classifier content classification model.
"""
def __init__(self, path: str = "."):
"""
Initialize the handler.
Args:
path (str): Path to the model files
"""
self.path = path
# Load model metadata to find the best model
summary_path = os.path.join(path, "model_summary.json")
try:
# Load model summary to get best model info
with open(summary_path, 'r') as f:
summary = json.load(f)
# Get the best model (rank 1)
best_model_info = summary['top_3_models'][0] # rank 1 is the best
# Load the classifier
classifier_path = os.path.join(path, best_model_info['files']['classifier'])
with open(classifier_path, 'rb') as f:
self.classifier = pickle.load(f)
# Load the MATCHING Doc2Vec model (same one used to train this classifier)
doc2vec_path = os.path.join(path, best_model_info['files']['doc2vec'])
print(f"π Loading Doc2Vec from: {doc2vec_path}")
# Check if associated files exist
for suffix in ['.wv.vectors.npy', '.syn1neg.npy']:
associated_file = doc2vec_path + suffix
if os.path.exists(associated_file):
print(f"β
Found associated file: {associated_file}")
else:
print(f"β οΈ Missing associated file: {associated_file}")
try:
self.doc2vec_model = Doc2Vec.load(doc2vec_path)
except Exception as e:
print(f"β Failed to load Doc2Vec model: {str(e)}")
print(f"β Current working directory: {os.getcwd()}")
print(f"β Files in path {path}:")
if os.path.exists(path):
for f in os.listdir(path):
print(f" - {f}")
raise Exception(f"Failed to load Doc2Vec model from {doc2vec_path}: {str(e)}")
# Verify feature dimensions match
expected_features = self.doc2vec_model.vector_size
print(f"β
Doc2Vec vector size: {expected_features}")
print(f"β
Model config: {best_model_info['config']}")
# Double-check that vector_size matches config
config_vector_size = best_model_info['config']['vector_size']
if expected_features != config_vector_size:
raise Exception(f"β Vector size mismatch! Doc2Vec model has {expected_features} but config says {config_vector_size}")
print(f"β
Vector dimensions verified: {expected_features} features")
# Additional debugging - test a sample vector
test_text = "test sample text for debugging"
test_vector = self.get_document_vector_for_test(test_text)
print(f"π Test vector shape: {test_vector.shape}")
print(f"π Classifier type: {type(self.classifier).__name__}")
# Store model info for reference
self.model_info = best_model_info
print(f"β
Loaded best model: {best_model_info['classifier']} ({best_model_info['algorithm_type']})")
print(f"β
Model accuracy: {best_model_info['accuracy']:.4f}")
print(f"β
Doc2Vec config: {best_model_info['config']}")
except Exception as e:
print(f"β Error details: {str(e)}")
print(f"β Error type: {type(e)}")
raise Exception(f"β Failed to load models: {str(e)}")
def __call__(self, data: Dict[str, Any]) -> Dict[str, Any]:
"""
Process inference request.
Args:
data (Dict): Request payload
Expected input formats:
Single prediction:
{
"inputs": {
"content": "text content",
"meta_description": "meta text"
}
}
Returns:
Dict: Prediction results
"""
try:
# Extract inputs from request
inputs = data.get("inputs", {})
if not inputs:
return {"error": "Missing 'inputs' in request data"}
# Handle single request
if isinstance(inputs, dict):
# Single prediction
return self._predict_single(inputs)
else:
return {"error": "Invalid input format. Expected dict."}
except Exception as e:
return {"error": f"Prediction failed: {str(e)}"}
def _predict_single(self, inputs: Dict[str, str]) -> Dict[str, Any]:
"""Handle single prediction request"""
content = inputs.get("content", "")
meta_description = inputs.get("meta_description", "")
if not content:
return {"error": "Missing required field: 'content'"}
# Ensure inputs are strings
content = str(content)
meta_description = str(meta_description)
try:
# Combine text features (same as training)
combined_text = content + ' ' + meta_description
# Preprocess text for Doc2Vec (same as training preprocessing)
preprocessed_words = self._preprocess_text(combined_text)
if not preprocessed_words:
# Handle empty text case
doc_vector = np.zeros(self.doc2vec_model.vector_size)
else:
# Get document vector from Doc2Vec model
doc_vector = self.doc2vec_model.infer_vector(preprocessed_words)
# Reshape for classifier (expects 2D array)
doc_vector = doc_vector.reshape(1, -1)
# Get predictions from classifier
probabilities = self.classifier.predict_proba(doc_vector)[0]
# Get class names from classifier
classes = self.classifier.classes_
# Create results in the same format as original handler
results = []
for i, label in enumerate(classes):
results.append({
"label": label,
"score": float(probabilities[i])
})
return results
except Exception as e:
return {"error": f"Single prediction failed: {str(e)}"}
def _preprocess_text(self, text):
"""Clean and preprocess text for Doc2Vec (same as training)"""
if pd.isna(text) or not text:
return []
# Convert to lowercase and remove special characters
text = str(text).lower()
text = re.sub(r'[^a-zA-Z\s]', '', text)
text = re.sub(r'\s+', ' ', text).strip()
# Split into words
words = text.split()
# Remove very short words
words = [word for word in words if len(word) > 2]
return words
def get_document_vector_for_test(self, text):
"""Get document vector for testing (same as _preprocess_text + infer_vector)"""
words = self._preprocess_text(text)
if not words:
return np.zeros(self.doc2vec_model.vector_size)
else:
return self.doc2vec_model.infer_vector(words)
# For local testing
if __name__ == "__main__":
# Test the handler locally
handler = EndpointHandler(".")
# Test single prediction
test_single = {
"inputs": {
"content": "Machine learning models for content attribution analysis using Doc2Vec embeddings",
"meta_description": "A comprehensive guide to ML-based content classification with neural networks"
}
}
print("=== Single Prediction Test ===")
result = handler(test_single)
print(result) |