subhan971 commited on
Commit
b2fc91c
·
verified ·
1 Parent(s): 08163e3

Update train.py

Browse files
Files changed (1) hide show
  1. train.py +332 -145
train.py CHANGED
@@ -1,14 +1,49 @@
1
-
 
 
 
 
 
 
2
 
3
  import os
 
4
  import httpx
 
5
  import numpy as np
6
- from sklearn.metrics.pairwise import cosine_similarity
 
 
 
 
7
 
 
8
  SUPABASE_URL = os.getenv('SUPABASE_URL', 'https://nquhiryqtbrtpauuxmsc.supabase.co')
9
  SERVICE_KEY = os.getenv('SUPABASE_SERVICE_KEY', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im5xdWhpcnlxdGJydHBhdXV4bXNjIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc3NjA4MzcxMCwiZXhwIjoyMDkxNjU5NzEwfQ.q7ysbaSfMv15g7PyJRLMwnRpEyia5d_ol3iLZn0Xayo')
10
  HEADERS = {'apikey': SERVICE_KEY, 'Content-Type': 'application/json'}
11
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  def safe_str(val):
13
  """Safe string conversion"""
14
  if val is None:
@@ -18,164 +53,316 @@ def safe_str(val):
18
  return str(val).strip().lower()
19
 
20
 
21
- async def test_recommend():
22
- """Test the recommend flow step by step"""
23
-
24
- print("\n" + "="*70)
25
- print("🔍 DEBUGGING RECOMMEND ENDPOINT")
26
- print("="*70)
27
-
28
- user_id = "ef7f794d-c323-4e5b-9bfa-40bc8d3f5d96"
29
-
30
- # Step 1: Fetch user
31
- print(f"\n1️⃣ Fetching user {user_id}...")
32
  try:
33
- resp = httpx.get(
34
- f"{SUPABASE_URL}/rest/v1/profiles",
35
- headers=HEADERS,
36
- params={'id': f'eq.{user_id}', 'select': '*'},
37
- timeout=10
38
- )
39
- print(f" Status: {resp.status_code}")
40
-
41
- if resp.status_code != 200:
42
- print(f" ✗ Error fetching user: {resp.status_code}")
43
- print(f" Response: {resp.text}")
44
- return
45
-
46
- users = resp.json()
47
- if not users:
48
- print(f" ✗ User not found in database")
49
- return
50
-
51
- user = users[0]
52
- print(f" ✓ User found: {user.get('full_name', 'Unknown')}")
53
- print(f" ✓ Gender: {user.get('gender', 'unknown')}")
54
- print(f" ✓ Age: {user.get('age', 'unknown')}")
55
-
56
- except Exception as e:
57
- print(f" ✗ Exception: {e}")
58
- return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
 
60
- # Step 2: Check gender
61
- print(f"\n2️⃣ Validating gender...")
62
- user_gender = safe_str(user.get('gender'))
63
- print(f" Gender: {user_gender}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
 
65
- if user_gender not in ('male', 'female'):
66
- print(f" ✗ Invalid gender: {user_gender}")
67
- return
68
- print(f" ✓ Valid gender")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
 
70
- opposite_gender = 'female' if user_gender == 'male' else 'male'
71
- print(f" ✓ Looking for: {opposite_gender}")
 
 
 
 
 
 
72
 
73
- # Step 3: Fetch all profiles
74
- print(f"\n3️⃣ Fetching all profiles...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  try:
76
- all_profiles = []
77
- offset = 0
 
78
 
79
- while offset < 1000:
80
- resp = httpx.get(
81
- f"{SUPABASE_URL}/rest/v1/profiles",
82
- headers=HEADERS,
83
- params={
84
- 'select': '*',
85
- 'is_active': 'eq.true',
86
- 'limit': 100,
87
- 'offset': offset
88
- },
89
- timeout=15
90
- )
91
-
92
- if resp.status_code != 200:
93
- print(f" ✗ Error at offset {offset}: {resp.status_code}")
94
- break
95
-
96
- batch = resp.json()
97
- if not batch:
98
- break
99
-
100
- all_profiles.extend(batch)
101
- offset += 100
102
- print(f" ✓ Fetched {len(all_profiles)} profiles...", end='\r')
103
-
104
- print(f"\n ✓ Total profiles: {len(all_profiles)}")
105
 
 
106
  except Exception as e:
107
- print(f" ✗ Exception: {e}")
108
- return
 
 
 
 
 
 
 
109
 
110
- # Step 4: Filter candidates
111
- print(f"\n4️⃣ Filtering candidates (opposite gender, active, not self)...")
112
- candidates = []
113
- for i, p in enumerate(all_profiles):
114
- p_gender = safe_str(p.get('gender'))
115
- p_active = p.get('is_active')
116
- p_id = p.get('id')
 
 
 
117
 
118
- if p_gender == opposite_gender and p_active and p_id != user_id:
119
- candidates.append((i, p))
120
-
121
- print(f" ✓ Found {len(candidates)} candidates")
 
 
 
 
 
 
 
 
 
122
 
123
- if len(candidates) < 1:
124
- print(f" ✗ No candidates found!")
125
  return
126
 
127
- # Step 5: Check profile data quality
128
- print(f"\n5️⃣ Checking profile data quality...")
129
-
130
- # Check user profile
131
- print(f"\n User profile fields:")
132
- for key in ['age', 'religion', 'marital_status', 'qualification', 'country', 'maslak', 'region_caste', 'hobbies', 'personality_traits', 'preferred_partner_criteria']:
133
- val = user.get(key)
134
- print(f" {key:30} = {str(val)[:50]}")
135
-
136
- # Check first candidate
137
- if candidates:
138
- print(f"\n First candidate profile fields:")
139
- cand = candidates[0][1]
140
- for key in ['age', 'religion', 'marital_status', 'qualification', 'country', 'maslak', 'region_caste', 'hobbies', 'personality_traits', 'preferred_partner_criteria']:
141
- val = cand.get(key)
142
- print(f" {key:30} = {str(val)[:50]}")
143
-
144
- # Step 6: Test feature encoding
145
- print(f"\n6️⃣ Testing feature encoding...")
146
-
147
- CATEGORICAL_COLS = ['religion', 'marital_status', 'qualification', 'country', 'maslak', 'region_caste']
148
- TEXT_COLS = ['hobbies', 'personality_traits', 'preferred_partner_criteria']
149
-
150
- try:
151
- # Age
152
- age = int(user.get('age') or 25)
153
- print(f" ✓ Age: {age}")
154
-
155
- # Categorical
156
- for col in CATEGORICAL_COLS:
157
- val = safe_str(user.get(col)) or 'unknown'
158
- print(f" ✓ {col}: {val}")
159
-
160
- # Text
161
- for col in TEXT_COLS:
162
- val = safe_str(user.get(col))
163
- print(f" ✓ {col}: {val[:50] if val else '(empty)'}...")
164
-
165
- except Exception as e:
166
- print(f" ✗ Encoding error: {e}")
167
- import traceback
168
- traceback.print_exc()
169
  return
170
-
171
- print(f"\n✅ All checks passed!")
172
- print(f"\nThe recommend endpoint should work. Check:")
173
- print(f"1. Logs in HF Space for detailed error")
174
- print(f"2. Verify SUPABASE_SERVICE_KEY is valid")
175
- print(f"3. Check /health endpoint for cache status")
176
- print(f"4. Try POST /refresh-cache to force update")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
 
178
 
179
  if __name__ == '__main__':
180
- import asyncio
181
- asyncio.run(test_recommend())
 
1
+ """
2
+ Enhanced Training Script with Online Learning
3
+ Features: age, religion, marital_status, qualification, country, maslak, region_caste,
4
+ hobbies, personality_traits, preferred_partner_criteria
5
+ Supports: Initial training, Incremental updates, Feature validation
6
+ Saves: encoders, tfidf_vectorizers, scaler, feature matrix, profile data
7
+ """
8
 
9
  import os
10
+ import pickle
11
  import httpx
12
+ import json
13
  import numpy as np
14
+ from datetime import datetime
15
+ from sklearn.feature_extraction.text import TfidfVectorizer
16
+ from sklearn.preprocessing import LabelEncoder, MinMaxScaler
17
+ from sklearn.neighbors import NearestNeighbors
18
+ import time
19
 
20
+ # Config
21
  SUPABASE_URL = os.getenv('SUPABASE_URL', 'https://nquhiryqtbrtpauuxmsc.supabase.co')
22
  SERVICE_KEY = os.getenv('SUPABASE_SERVICE_KEY', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im5xdWhpcnlxdGJydHBhdXV4bXNjIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc3NjA4MzcxMCwiZXhwIjoyMDkxNjU5NzEwfQ.q7ysbaSfMv15g7PyJRLMwnRpEyia5d_ol3iLZn0Xayo')
23
  HEADERS = {'apikey': SERVICE_KEY, 'Content-Type': 'application/json'}
24
 
25
+ CATEGORICAL_COLS = ['religion', 'marital_status', 'qualification', 'country', 'maslak', 'region_caste']
26
+ TEXT_COLS = ['hobbies', 'personality_traits', 'preferred_partner_criteria']
27
+ MODEL_PATH = 'models/recommendation_model.pkl'
28
+ TRAINED_IDS_PATH = 'models/trained_profile_ids.json'
29
+
30
+
31
+ def fetch_profiles(limit=1000):
32
+ """Fetch active profiles from Supabase"""
33
+ url = f"{SUPABASE_URL}/rest/v1/profiles"
34
+ params = {'select': '*', 'is_active': 'eq.true', 'limit': str(limit)}
35
+
36
+ try:
37
+ resp = httpx.get(url, headers=HEADERS, params=params, timeout=30)
38
+ resp.raise_for_status()
39
+ data = resp.json()
40
+ print(f"[OK] Fetched {len(data)} profiles from Supabase")
41
+ return data
42
+ except Exception as e:
43
+ print(f"[X] Error fetching profiles: {e}")
44
+ return []
45
+
46
+
47
  def safe_str(val):
48
  """Safe string conversion"""
49
  if val is None:
 
53
  return str(val).strip().lower()
54
 
55
 
56
+ def preprocess(p):
57
+ """Convert raw Supabase profile to feature-ready dict"""
 
 
 
 
 
 
 
 
 
58
  try:
59
+ age = int(p.get('age') or 25)
60
+ except:
61
+ age = 25
62
+
63
+ cat = {col: safe_str(p.get(col)) or 'unknown' for col in CATEGORICAL_COLS}
64
+ text = {col: safe_str(p.get(col)) for col in TEXT_COLS}
65
+
66
+ return {
67
+ 'age': age,
68
+ **cat,
69
+ **text,
70
+ 'gender': p.get('gender', '').lower(),
71
+ 'is_active': p.get('is_active', True),
72
+ 'id': p['id'],
73
+ 'full_name': p.get('full_name', 'Unknown'),
74
+ 'city': p.get('city', ''),
75
+ 'profile_picture_url': p.get('profile_picture_url')
76
+ }
77
+
78
+
79
+ def load_trained_ids():
80
+ """Load IDs of profiles already in model"""
81
+ if os.path.exists(TRAINED_IDS_PATH):
82
+ try:
83
+ with open(TRAINED_IDS_PATH, 'r') as f:
84
+ return set(json.load(f))
85
+ except:
86
+ return set()
87
+ return set()
88
+
89
+
90
+ def save_trained_ids(ids):
91
+ """Save trained profile IDs"""
92
+ os.makedirs('models', exist_ok=True)
93
+ with open(TRAINED_IDS_PATH, 'w') as f:
94
+ json.dump(list(ids), f)
95
+
96
+
97
+ def load_model():
98
+ """Load existing model"""
99
+ if os.path.exists(MODEL_PATH):
100
+ try:
101
+ with open(MODEL_PATH, 'rb') as f:
102
+ model = pickle.load(f)
103
+ print(f"[OK] Loaded existing model: {model['n_profiles']} profiles")
104
+ return model
105
+ except Exception as e:
106
+ print(f"[X] Error loading model: {e}")
107
+ return None
108
+
109
+
110
+ def encode_features_initial(profiles):
111
+ """Initial training: Encode all features"""
112
+ n = len(profiles)
113
+
114
+ # LabelEncode categoricals
115
+ le_dict = {}
116
+ cat_matrix = []
117
+ for col in CATEGORICAL_COLS:
118
+ le = LabelEncoder()
119
+ values = [p[col] for p in profiles]
120
+ le.fit(values)
121
+ le_dict[col] = le
122
+ encoded = le.transform(values)
123
+ cat_matrix.append(encoded.reshape(-1, 1))
124
+
125
+ # TF-IDF for text columns
126
+ tfidf_vectors = []
127
+ tfidf_dict = {}
128
+ for col in TEXT_COLS:
129
+ corpus = [p[col] for p in profiles]
130
+ tfidf = TfidfVectorizer(max_features=20, stop_words='english', token_pattern=r'(?u)\\b\\w+\\b')
131
+ vec = tfidf.fit_transform(corpus).toarray()
132
+ tfidf_dict[col] = tfidf
133
+ tfidf_vectors.append(vec)
134
+
135
+ # Normalize age
136
+ ages = np.array([[p['age']] for p in profiles], dtype=float)
137
+ scaler = MinMaxScaler()
138
+ ages_norm = scaler.fit_transform(ages)
139
+
140
+ # Combine: age (1) + cat (6) + tfidf(3*20=60)
141
+ feature_matrix = np.hstack([ages_norm] + cat_matrix + tfidf_vectors)
142
 
143
+ return feature_matrix, le_dict, scaler, tfidf_dict
144
+
145
+
146
+ def encode_single_profile(profile, le_dict, scaler, tfidf_dict):
147
+ """Encode single profile using existing transformers"""
148
+ features = []
149
+
150
+ # Age
151
+ age_norm = scaler.transform([[float(profile['age'])]])[0]
152
+ features.extend(age_norm)
153
+
154
+ # Categoricals
155
+ for col in CATEGORICAL_COLS:
156
+ le = le_dict[col]
157
+ val = profile[col]
158
+ try:
159
+ encoded = le.transform([val])[0:1]
160
+ except ValueError:
161
+ if 'unknown' in le.classes_:
162
+ encoded = le.transform(['unknown'])[0:1]
163
+ else:
164
+ encoded = le.transform([le.classes_[0]])[0:1]
165
+ features.extend(encoded)
166
+
167
+ # Text fields
168
+ for col in TEXT_COLS:
169
+ tfidf = tfidf_dict[col]
170
+ vec = tfidf.transform([profile[col]]).toarray()[0]
171
+ features.extend(vec)
172
+
173
+ return np.array(features).reshape(1, -1)
174
+
175
+
176
+ def train_initial(profiles):
177
+ """Initial training on all profiles"""
178
+ print("\n" + "="*60)
179
+ print("INITIAL TRAINING")
180
+ print("="*60)
181
 
182
+ start_time = time.time()
183
+
184
+ X, le_dict, scaler, tfidf_dict = encode_features_initial(profiles)
185
+
186
+ print(f"\n[OK] Feature encoding completed")
187
+ print(f" - Categorical cols: {len(CATEGORICAL_COLS)}")
188
+ print(f" - Text cols: {len(TEXT_COLS)}")
189
+ print(f" - Feature matrix: {X.shape}")
190
+
191
+ n_neighbors = min(10, len(profiles)-1) if len(profiles) > 1 else 1
192
+ nn = NearestNeighbors(n_neighbors=n_neighbors, metric='cosine', algorithm='brute')
193
+ nn.fit(X)
194
+ print(f"[OK] NearestNeighbors trained (n_neighbors={n_neighbors})")
195
+
196
+ model = {
197
+ 'version': '2.0',
198
+ 'trained_at': datetime.now().isoformat(),
199
+ 'n_profiles': len(profiles),
200
+ 'feature_matrix': X,
201
+ 'profile_ids': [p['id'] for p in profiles],
202
+ 'profile_data': profiles,
203
+ 'label_encoders': le_dict,
204
+ 'scaler': scaler,
205
+ 'tfidf_vectorizers': tfidf_dict,
206
+ 'nn_model': nn
207
+ }
208
+
209
+ duration = time.time() - start_time
210
+ print(f"[OK] Training completed in {duration:.2f}s")
211
 
212
+ return model
213
+
214
+
215
+ def train_incremental(new_profiles, old_model):
216
+ """Incremental training: add only new profiles"""
217
+ print("\n" + "="*60)
218
+ print("INCREMENTAL UPDATE")
219
+ print("="*60)
220
 
221
+ start_time = time.time()
222
+
223
+ old_X = old_model['feature_matrix']
224
+ old_profiles = old_model['profile_data']
225
+ old_ids = set(old_model['profile_ids'])
226
+
227
+ new_only = [p for p in new_profiles if p['id'] not in old_ids]
228
+
229
+ if not new_only:
230
+ print(f"\n[INFO] No new profiles to add")
231
+ return old_model, 0
232
+
233
+ print(f"\n[OK] Found {len(new_only)} new profiles")
234
+
235
+ new_features = []
236
+ for p in new_only:
237
+ feat = encode_single_profile(
238
+ p,
239
+ old_model['label_encoders'],
240
+ old_model['scaler'],
241
+ old_model['tfidf_vectorizers']
242
+ )
243
+ new_features.append(feat[0])
244
+
245
+ new_X = np.vstack(new_features)
246
+ combined_X = np.vstack([old_X, new_X])
247
+ combined_profiles = old_profiles + new_only
248
+ combined_ids = [p['id'] for p in combined_profiles]
249
+
250
+ print(f"[OK] Feature matrix merged")
251
+ print(f" - Old: {len(old_profiles)}")
252
+ print(f" - New: {len(new_only)}")
253
+ print(f" - Total: {len(combined_profiles)}")
254
+ print(f" - Matrix: {combined_X.shape}")
255
+
256
+ n_neighbors = min(10, len(combined_profiles)-1)
257
+ nn = NearestNeighbors(n_neighbors=n_neighbors, metric='cosine', algorithm='brute')
258
+ nn.fit(combined_X)
259
+ print(f"[OK] NearestNeighbors retrained")
260
+
261
+ updated_model = {
262
+ 'version': '2.0',
263
+ 'trained_at': datetime.now().isoformat(),
264
+ 'n_profiles': len(combined_profiles),
265
+ 'feature_matrix': combined_X,
266
+ 'profile_ids': combined_ids,
267
+ 'profile_data': combined_profiles,
268
+ 'label_encoders': old_model['label_encoders'],
269
+ 'scaler': old_model['scaler'],
270
+ 'tfidf_vectorizers': old_model['tfidf_vectorizers'],
271
+ 'nn_model': nn
272
+ }
273
+
274
+ duration = time.time() - start_time
275
+ print(f"[OK] Update completed in {duration:.2f}s")
276
+
277
+ return updated_model, len(new_only)
278
+
279
+
280
+ def save_model(model):
281
+ """Save model to disk"""
282
+ os.makedirs('models', exist_ok=True)
283
  try:
284
+ with open(MODEL_PATH, 'wb') as f:
285
+ pickle.dump(model, f)
286
+ print(f"[OK] Model saved to {MODEL_PATH}")
287
 
288
+ save_trained_ids(model['profile_ids'])
289
+ print(f"[OK] Trained IDs saved")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
290
 
291
+ return True
292
  except Exception as e:
293
+ print(f"[X] Error saving model: {e}")
294
+ return False
295
+
296
+
297
+ def validate_model(model, num_tests=3):
298
+ """Validate model on sample profiles"""
299
+ print("\n" + "="*60)
300
+ print("VALIDATION TEST")
301
+ print("="*60)
302
 
303
+ profiles = model['profile_data']
304
+ X = model['feature_matrix']
305
+ nn = model['nn_model']
306
+
307
+ for idx in range(min(num_tests, len(profiles))):
308
+ test_vec = X[idx:idx+1]
309
+ distances, indices = nn.kneighbors(test_vec, n_neighbors=min(5, len(profiles)))
310
+
311
+ name = profiles[idx]['full_name']
312
+ print(f"\nTop matches for {name}:")
313
 
314
+ for rank, (dist, match_idx) in enumerate(zip(distances[0], indices[0]), 1):
315
+ sim_score = (1 - dist) * 100
316
+ match_name = profiles[match_idx]['full_name']
317
+ print(f" {rank}. {match_name} - {sim_score:.1f}%")
318
+
319
+
320
+ def main():
321
+ print("\n" + "="*70)
322
+ print("MATCHMAKING MODEL TRAINING")
323
+ print("="*70)
324
+
325
+ print("\n[1] Fetching profiles from Supabase...")
326
+ raw_profiles = fetch_profiles()
327
 
328
+ if not raw_profiles:
329
+ print("[X] No profiles fetched. Exiting.")
330
  return
331
 
332
+ if len(raw_profiles) < 10:
333
+ print("[X] Need at least 10 profiles. Got " + str(len(raw_profiles)))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
334
  return
335
+
336
+ print("[2] Preprocessing profiles...")
337
+ profiles = [preprocess(p) for p in raw_profiles]
338
+ print("[OK] Preprocessed " + str(len(profiles)) + " profiles")
339
+
340
+ existing_model = load_model()
341
+
342
+ if existing_model is None:
343
+ model = train_initial(profiles)
344
+ else:
345
+ model, new_count = train_incremental(profiles, existing_model)
346
+ if new_count == 0:
347
+ print("\n[INFO] No updates needed. Model is current.")
348
+ return
349
+
350
+ print("\n[3] Saving model...")
351
+ if not save_model(model):
352
+ print("[X] Failed to save model")
353
+ return
354
+
355
+ validate_model(model)
356
+
357
+ print("\n" + "="*70)
358
+ print("TRAINING COMPLETED SUCCESSFULLY!")
359
+ print("="*70)
360
+ print("\nSummary:")
361
+ print(" - Total profiles: " + str(model['n_profiles']))
362
+ print(" - Feature dimensions: " + str(model['feature_matrix'].shape[1]))
363
+ print(" - Model version: " + model['version'])
364
+ print(" - Trained at: " + model['trained_at'])
365
 
366
 
367
  if __name__ == '__main__':
368
+ main()