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)