import numpy as np import pandas as pd import sklearn from sklearn.neighbors import KNeighborsClassifier from sklearn.model_selection import train_test_split, cross_val_score from sklearn.metrics import classification_report, confusion_matrix import matplotlib.pyplot as plt import seaborn as sns import os import re import joblib from sklearn.preprocessing import LabelEncoder import gc # Garbage collector from sklearn.utils.class_weight import compute_class_weight # 1. Load the embeddings and image paths print("Loading embeddings and image paths...") features = np.load('../extracted-features-test/features.npy') with open('../extracted-features-test/image_paths.txt', 'r') as f: image_paths = [line.strip() for line in f.readlines()] print(f"Loaded {len(features)} embedding vectors with shape {features.shape}") # 2. Extract TCGA case IDs from image paths print("Extracting TCGA case IDs...") tcga_cases = [] for path in image_paths: # Extract the TCGA-XX-XXXX-XXX-XX-XXX pattern match = re.search(r'(TCGA-[A-Z0-9]{2}-[A-Z0-9]{4}-[0-9A-Z]{3}-[0-9A-Z]{2}-[A-Z0-9]{3})', path) if match: tcga_case = match.group(1) else: # If the full pattern is not found, try extracting at least the TCGA-XX-XXXX part match = re.search(r'(TCGA-[A-Z0-9]{2}-[A-Z0-9]{4})', path) if match: tcga_case = match.group(1) else: tcga_case = os.path.basename(os.path.dirname(path)) # Get folder name from the file path tcga_cases.append(tcga_case) # 3. Load the CSV data with patient information csv_path = 'E:\\buse_thesis_prostate\\algorithms\\dinov1\\dino-code\\tcga\\prostate_tcga_wsi_paths_aws_DX_only.csv' # Update the CSV file path df = pd.read_csv(csv_path) print(f"CSV loaded with {len(df)} rows") # 4. Match patch images with case IDs and define the label per case print("Creating case-to-grade mapping...") case_to_grade = {} for idx, row in df.iterrows(): filename = row['filename'] grade = row['gleason_grade'] # Extract TCGA case ID from the CSV 'filename' match = re.search(r'(TCGA-[A-Z0-9]{2}-[A-Z0-9]{4})', filename) if match: case_id = match.group(1) case_to_grade[case_id] = grade print(f"Created mapping for {len(case_to_grade)} unique cases") # 5. Match each embedding vector with its corresponding grade print("Matching embeddings with grades...") matched_features = [] matched_labels = [] matched_cases = [] # Process in batches for memory management batch_size = 10000 num_batches = len(tcga_cases) // batch_size + (1 if len(tcga_cases) % batch_size > 0 else 0) for batch_idx in range(num_batches): start_idx = batch_idx * batch_size end_idx = min((batch_idx + 1) * batch_size, len(tcga_cases)) print(f"Processing batch {batch_idx+1}/{num_batches} (samples {start_idx}-{end_idx})...") batch_features = [] batch_labels = [] batch_cases = [] for i in range(start_idx, end_idx): case_id = tcga_cases[i] # First, try matching using the full case ID if case_id in case_to_grade: batch_features.append(features[i]) batch_labels.append(case_to_grade[case_id]) batch_cases.append(case_id) else: # Try matching using the short version of the case ID (TCGA-XX-XXXX) short_match = re.search(r'(TCGA-[A-Z0-9]{2}-[A-Z0-9]{4})', case_id) if short_match: short_id = short_match.group(1) if short_id in case_to_grade: batch_features.append(features[i]) batch_labels.append(case_to_grade[short_id]) batch_cases.append(short_id) # Append the batch to the main lists matched_features.extend(batch_features) matched_labels.extend(batch_labels) matched_cases.extend(batch_cases) # Clear batch memory del batch_features, batch_labels, batch_cases gc.collect() # 6. Reduce data size: subsample a limited number of examples per class print(f"Total matched samples before subsampling: {len(matched_features)}") # Convert labels to numeric IDs using LabelEncoder (to simplify processing) label_encoder = LabelEncoder() numeric_labels = label_encoder.fit_transform(matched_labels) unique_labels = label_encoder.classes_ # Maximum number of samples per class max_samples_per_class = 20000 # Adjust this value depending on your available memory # Perform subsampling sampled_indices = [] for label in np.unique(numeric_labels): # Find indices of all examples belonging to this class class_indices = np.where(numeric_labels == label)[0] # Random sampling (if the class has fewer than max_samples_per_class examples, take them all) if len(class_indices) > max_samples_per_class: sampled_idx = np.random.choice(class_indices, max_samples_per_class, replace=False) else: sampled_idx = class_indices sampled_indices.extend(sampled_idx) # Create subsampled datasets X = np.array([matched_features[i] for i in sampled_indices]) y = np.array([matched_labels[i] for i in sampled_indices]) sampled_cases = [matched_cases[i] for i in sampled_indices] print(f"Subsampled to {len(X)} total examples") print(f"Unique Gleason grades: {np.unique(y)}") print(f"Class distribution after subsampling:") for grade in np.unique(y): print(f" Grade {grade}: {np.sum(y == grade)} samples") # Clear memory del matched_features, matched_labels, matched_cases gc.collect() # 7. Split into training and testing sets X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y) # ignoring the specific class mask = y_train != '2+4' X_train = X_train[mask] y_train = y_train[mask] mask_test = y_test != '2+4' X_test = X_test[mask_test] y_test = y_test[mask_test] print(f"Training set: {X_train.shape[0]} samples") print(f"Test set: {X_test.shape[0]} samples") # Calculate class weights class_weights = compute_class_weight('balanced', classes=np.unique(y_train), y=y_train) class_weight_dict = dict(zip(np.unique(y_train), class_weights)) print("Class weights:", class_weight_dict) # 8. Train a simple KNN model (instead of GridSearchCV) print("Training KNN model...") # Try several k values one by one (without GridSearchCV) k_values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] best_k = 5 # Default value best_score = 0 for k in k_values: # Basit cross-validation knn = KNeighborsClassifier(n_neighbors=k, weights='distance', metric='cosine') scores = cross_val_score(knn, X_train, y_train, cv=3, scoring='accuracy') avg_score = np.mean(scores) print(f"k={k}, Cross-validation score: {avg_score:.4f}") if avg_score > best_score: best_score = avg_score best_k = k print(f"Best k value: {best_k} with score: {best_score:.4f}") # Train the final model final_model = KNeighborsClassifier( n_neighbors=best_k, weights='distance', metric='cosine' ) final_model.fit(X_train, y_train) # 9. Evaluate on test set y_pred = final_model.predict(X_test) print("\nClassification Report:") print(classification_report(y_test, y_pred)) # 10. Create confusion matrix cm = confusion_matrix(y_test, y_pred) plt.figure(figsize=(10, 8)) sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=np.unique(y_test), # Change here yticklabels=np.unique(y_test)) # And here plt.xlabel('Predicted') plt.ylabel('Actual') plt.title('Confusion Matrix') plt.savefig('confusion_matrix.png') print("Saved confusion matrix to confusion_matrix.png") print("ROC AUC", sklearn.metrics.roc_auc_score) # 11. Save the trained model joblib.dump(final_model, 'tcga_gleason_knn_model.joblib') print("Model saved to tcga_gleason_knn_model.joblib") # 12. Save embedding-to-class mapping for future use result_df = pd.DataFrame({ 'case_id': sampled_cases, 'gleason_grade': y }) result_df.to_csv('matched_cases.csv', index=False) print("Saved case ID to class mapping in matched_cases.csv") # Save arrays np.save('X_train.npy', X_train) np.save('y_train.npy', y_train) # Save the LabelEncoder as well joblib.dump(label_encoder, 'label_encoder.pkl') # 13. Function to predict on new samples def predict_gleason_grade(embedding_vector, model_path='tcga_gleason_knn_model.joblib'): """Predict Gleason grade for a new DINO embedding vector""" model = joblib.load(model_path) # Reshape to ensure 2D array embedding_vector = np.array(embedding_vector).reshape(1, -1) prediction = model.predict(embedding_vector) probabilities = model.predict_proba(embedding_vector) return { 'predicted_grade': prediction[0], 'probabilities': dict(zip(model.classes_, probabilities[0])) } print("\nDone! You can now use the trained model for predictions.") # Assuming y is your array of class labels unique_classes, counts = np.unique(y, return_counts=True) # Create a dictionary to map class labels to their counts class_counts = dict(zip(unique_classes, counts)) # Print the counts for each class print("Total number of samples per class:") for class_label, count in class_counts.items(): print(f"Class {class_label}: {count} samples") # Print the total number of samples in the dataset total_samples = len(y) print(f"Total number of samples in the dataset: {total_samples}")