| import numpy as np
|
| import pandas as pd
|
| from sklearn.neural_network import MLPClassifier
|
| from sklearn.model_selection import train_test_split
|
| from sklearn.metrics import classification_report, confusion_matrix
|
| import matplotlib.pyplot as plt
|
| import seaborn as sns
|
| import joblib
|
| from sklearn.preprocessing import StandardScaler, LabelEncoder
|
| from collections import Counter
|
| from imblearn.over_sampling import SMOTE
|
|
|
| def load_features_from_extraction(output_dir):
|
| """
|
| Loads feature files produced by `extract_features_hierarchical.py`.
|
| No need to read an extra CSV - labels are already stored in `fused_features.csv`.
|
| """
|
| from pathlib import Path
|
| output_dir = Path(output_dir)
|
|
|
| features_path = output_dir / 'fused_features.npy'
|
| if not features_path.exists():
|
| raise FileNotFoundError(f"Feature file not found: {features_path}")
|
|
|
| print(f"Loading features from {features_path}")
|
| features = np.load(features_path)
|
| print(f"Loaded {len(features)} features with shape {features.shape}")
|
|
|
| csv_path = output_dir / 'fused_features.csv'
|
| if not csv_path.exists():
|
| raise FileNotFoundError(f"CSV file not found: {csv_path}")
|
|
|
| print(f"Loading labels from {csv_path}")
|
| df = pd.read_csv(csv_path)
|
|
|
| if 'label' in df.columns:
|
| labels = df['label'].values
|
| valid_mask = labels != -1
|
| elif 'gleason_grade' in df.columns:
|
| from sklearn.preprocessing import LabelEncoder
|
| label_encoder = LabelEncoder()
|
| grades = df['gleason_grade'].values
|
| valid_mask = (grades != 'unknown') & (pd.notna(grades))
|
| valid_grades = grades[valid_mask]
|
| labels = np.full(len(grades), -1)
|
| if len(valid_grades) > 0:
|
| labels[valid_mask] = label_encoder.fit_transform(valid_grades)
|
| else:
|
| raise ValueError("Could not find the 'label' or 'gleason_grade' column in the CSV!")
|
|
|
| grades = df['gleason_grade'].values if 'gleason_grade' in df.columns else None
|
| patient_ids = df['patient_id'].values if 'patient_id' in df.columns else None
|
|
|
| features = features[valid_mask]
|
|
|
| if grades is not None:
|
| labels = grades[valid_mask]
|
| else:
|
| labels = labels[valid_mask]
|
| grades = grades[valid_mask] if grades is not None else None
|
| patient_ids = patient_ids[valid_mask] if patient_ids is not None else None
|
|
|
| print(f"Total matched samples: {len(features)}")
|
| if grades is not None:
|
| print(f"Unique Gleason grades: {np.unique(grades)}")
|
|
|
| return features, labels, grades, patient_ids
|
|
|
| def create_weighted_mlp_classifier(y_train_encoded, label_encoder):
|
| """
|
| Create a weighted MLP classifier based on class performance.
|
| """
|
|
|
| class_counts = Counter(y_train_encoded)
|
| total_samples = len(y_train_encoded)
|
|
|
|
|
| class_weights = {}
|
| for class_id in range(len(label_encoder.classes_)):
|
| if class_id in class_counts:
|
|
|
| weight = total_samples / (len(class_counts) * class_counts[class_id])
|
| class_weights[class_id] = weight
|
| else:
|
| class_weights[class_id] = 1.0
|
|
|
| print("Class weights:")
|
| for class_id, weight in class_weights.items():
|
| class_name = label_encoder.inverse_transform([class_id])[0]
|
| print(f" {class_name}: {weight:.3f}")
|
|
|
|
|
| weighted_mlp = MLPClassifier(
|
|
|
| hidden_layer_sizes=(2048, 1024, 512, 256),
|
|
|
|
|
| activation='relu',
|
| solver='adam',
|
|
|
|
|
| alpha=0.00001,
|
|
|
|
|
| batch_size=128,
|
| learning_rate='adaptive',
|
| learning_rate_init=0.001,
|
| max_iter=500,
|
|
|
|
|
| early_stopping=True,
|
| validation_fraction=0.1,
|
| n_iter_no_change=20,
|
|
|
|
|
| random_state=42,
|
| verbose=True,
|
|
|
|
|
| momentum=0.9,
|
| nesterovs_momentum=True,
|
|
|
|
|
| power_t=0.5
|
| )
|
|
|
| return weighted_mlp, class_weights
|
|
|
| def create_sample_weights(y_train_encoded, class_weights):
|
| """
|
| Creates sample weights for each example.
|
| """
|
| sample_weights = np.ones(len(y_train_encoded))
|
| for i, class_id in enumerate(y_train_encoded):
|
| sample_weights[i] = class_weights.get(class_id, 1.0)
|
|
|
| print(f"Sample weights created - min: {sample_weights.min():.3f}, max: {sample_weights.max():.3f}")
|
| return sample_weights
|
|
|
| def create_weighted_dataset(X_train, y_train_encoded, class_weights, label_encoder):
|
| """
|
| Creates a weighted dataset by duplicating samples according to weights.
|
| """
|
| print("Creating weighted dataset by duplicating samples...")
|
|
|
|
|
| duplication_factors = {}
|
| for class_id, weight in class_weights.items():
|
|
|
| duplication_factors[class_id] = max(1, int(round(weight)))
|
|
|
| print("Duplication factors:")
|
| for class_id, factor in duplication_factors.items():
|
| class_name = label_encoder.inverse_transform([class_id])[0]
|
| print(f" {class_name}: {factor}x")
|
|
|
|
|
| X_weighted = []
|
| y_weighted = []
|
|
|
| for i, class_id in enumerate(y_train_encoded):
|
|
|
| X_weighted.append(X_train[i])
|
| y_weighted.append(class_id)
|
|
|
|
|
| extra_copies = duplication_factors.get(class_id, 1) - 1
|
| for _ in range(extra_copies):
|
| X_weighted.append(X_train[i])
|
| y_weighted.append(class_id)
|
|
|
| X_weighted = np.array(X_weighted)
|
| y_weighted = np.array(y_weighted)
|
|
|
| print(f"Weighted dataset created: {len(X_weighted)} samples (original: {len(X_train)})")
|
|
|
|
|
| new_counts = Counter(y_weighted)
|
| print("\nNew class distribution after weighting:")
|
| for class_id, count in new_counts.items():
|
| class_name = label_encoder.inverse_transform([class_id])[0]
|
| print(f" {class_name}: {count}")
|
|
|
| return X_weighted, y_weighted
|
|
|
| def apply_weighted_smote(X_train, y_train_encoded, label_encoder, sampling_strategy='auto'):
|
| """
|
| Applies weighted SMOTE - it does not fully balance classes; it only slightly increases underrepresented classes.
|
| """
|
| print("Applying weighted SMOTE...")
|
|
|
|
|
| original_counts = Counter(y_train_encoded)
|
| print("Original class distribution:")
|
| for class_id, count in original_counts.items():
|
| class_name = label_encoder.inverse_transform([class_id])[0]
|
| print(f" {class_name}: {count}")
|
|
|
|
|
| max_samples = max(original_counts.values())
|
| target_samples = int(max_samples * 0.8)
|
|
|
|
|
| if sampling_strategy == 'auto':
|
|
|
| sampling_strategy = {}
|
| for class_id in original_counts:
|
| current_count = original_counts[class_id]
|
| if current_count < target_samples:
|
| sampling_strategy[class_id] = target_samples
|
| else:
|
| sampling_strategy[class_id] = current_count
|
|
|
| print(f"Target samples per class: {target_samples}")
|
| print("Sampling strategy:", sampling_strategy)
|
|
|
|
|
| smote = SMOTE(sampling_strategy=sampling_strategy, random_state=42)
|
| X_resampled, y_resampled = smote.fit_resample(X_train, y_train_encoded)
|
|
|
|
|
| new_counts = Counter(y_resampled)
|
| print("\nNew class distribution after weighted SMOTE:")
|
| for class_id, count in new_counts.items():
|
| class_name = label_encoder.inverse_transform([class_id])[0]
|
| print(f" {class_name}: {count}")
|
|
|
| return X_resampled, y_resampled
|
|
|
| def main():
|
|
|
| print("=== LOADING TRAIN FEATURES ===")
|
| X_train, y_train, train_grades, train_cases = load_features_from_extraction(
|
| 'feature_extraction/extractedfusedfeatures_train'
|
| )
|
|
|
|
|
| print("\n=== LOADING TEST FEATURES ===")
|
| X_test, y_test, test_grades, test_cases = load_features_from_extraction(
|
| 'feature_extraction/extractedfusedfeatures_test'
|
| )
|
|
|
|
|
| print("\n=== FILTERING DATA ===")
|
| if train_grades is not None:
|
| train_mask = train_grades != '2+4'
|
| X_train = X_train[train_mask]
|
| y_train = y_train[train_mask]
|
| train_grades = train_grades[train_mask]
|
| train_cases = train_cases[train_mask] if train_cases is not None else None
|
|
|
| if test_grades is not None:
|
| test_mask = test_grades != '2+4'
|
| X_test = X_test[test_mask]
|
| y_test = y_test[test_mask]
|
| test_grades = test_grades[test_mask]
|
| test_cases = test_cases[test_mask] if test_cases is not None else None
|
|
|
| print(f"Final training set: {X_train.shape[0]} samples")
|
| print(f"Final test set: {X_test.shape[0]} samples")
|
| print(f"Training classes: {np.unique(y_train)}")
|
| print(f"Test classes: {np.unique(y_test)}")
|
|
|
|
|
| print("\n=== ENCODING LABELS ===")
|
| label_encoder = LabelEncoder()
|
| y_train_encoded = label_encoder.fit_transform(y_train)
|
| y_test_encoded = label_encoder.transform(y_test)
|
|
|
| print(f"Unique classes after encoding: {np.unique(y_train_encoded)}")
|
| print("Label mapping:", dict(zip(label_encoder.classes_, range(len(label_encoder.classes_)))))
|
|
|
|
|
| print("\nClass distribution before SMOTE:")
|
| print(Counter(y_train_encoded))
|
|
|
|
|
| plt.figure(figsize=(12, 6))
|
| class_counts = Counter(y_train_encoded)
|
| class_names = [label_encoder.inverse_transform([i])[0] for i in range(len(label_encoder.classes_))]
|
| class_values = [class_counts.get(i, 0) for i in range(len(label_encoder.classes_))]
|
|
|
| plt.bar(class_names, class_values, color='skyblue', edgecolor='black')
|
| plt.title('Class Distribution - Training Data')
|
| plt.xlabel('Gleason Grade')
|
| plt.ylabel('Number of Samples')
|
| plt.xticks(rotation=45)
|
| plt.tight_layout()
|
| plt.savefig('class_distribution_training.png')
|
| print("Saved class distribution plot: class_distribution_training.png")
|
| plt.show()
|
|
|
|
|
| print("\n=== APPLYING WEIGHTED SMOTE ===")
|
| X_train_resampled, y_train_resampled = apply_weighted_smote(X_train, y_train_encoded, label_encoder)
|
|
|
|
|
| print("\n=== CREATING WEIGHTED MLP MODEL ===")
|
| weighted_mlp, class_weights = create_weighted_mlp_classifier(y_train_resampled, label_encoder)
|
|
|
|
|
| print("\n=== CREATING WEIGHTED DATASET ===")
|
| X_train_weighted, y_train_weighted = create_weighted_dataset(X_train_resampled, y_train_resampled, class_weights, label_encoder)
|
|
|
|
|
| print("\n=== SCALING WEIGHTED FEATURES ===")
|
| scaler = StandardScaler()
|
| X_train_scaled = scaler.fit_transform(X_train_weighted)
|
| X_test_scaled = scaler.transform(X_test)
|
|
|
| print(f"\nFinal training set shape after weighting: {X_train_scaled.shape}")
|
|
|
|
|
| print("\n=== TRAINING WEIGHTED MLP MODEL ===")
|
| print("Starting model training...")
|
| weighted_mlp.fit(X_train_scaled, y_train_weighted)
|
|
|
|
|
| print("\n=== EVALUATING WEIGHTED MODEL ON TEST SET ===")
|
| y_pred = weighted_mlp.predict(X_test_scaled)
|
|
|
| y_test_original = label_encoder.inverse_transform(y_test_encoded)
|
| y_pred_original = label_encoder.inverse_transform(y_pred)
|
| print("\nClassification Report:")
|
| print(classification_report(y_test_original, y_pred_original))
|
|
|
|
|
| cm = confusion_matrix(y_test_original, y_pred_original)
|
| plt.figure(figsize=(12, 10))
|
| sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
|
| xticklabels=label_encoder.classes_,
|
| yticklabels=label_encoder.classes_)
|
| plt.xlabel('Predicted')
|
| plt.ylabel('Actual')
|
| plt.title('Weighted MLP Confusion Matrix - 90 Epoch Features')
|
| plt.xticks(rotation=45)
|
| plt.yticks(rotation=45)
|
| plt.tight_layout()
|
| plt.savefig('weighted_mlp_confusion_matrix_90ep.png')
|
| print("Saved confusion matrix to weighted_mlp_confusion_matrix_90ep.png")
|
|
|
|
|
| joblib.dump(weighted_mlp, 'tcga_gleason_weighted_mlp_model_90ep.joblib')
|
| joblib.dump(scaler, 'tcga_gleason_weighted_mlp_scaler_90ep.joblib')
|
| joblib.dump(label_encoder, 'tcga_gleason_weighted_label_encoder_90ep.joblib')
|
| joblib.dump(class_weights, 'tcga_gleason_class_weights_90ep.joblib')
|
| print("Weighted model, scaler, label encoder and class weights saved")
|
|
|
|
|
| plt.figure(figsize=(12, 8))
|
| plt.plot(weighted_mlp.loss_curve_)
|
| plt.title('Weighted MLP Learning Curve - 90 Epoch Features')
|
| plt.xlabel('Iterations')
|
| plt.ylabel('Loss')
|
| plt.grid(True)
|
| plt.savefig('weighted_mlp_learning_curve_90ep.png')
|
| print("Saved learning curve to weighted_mlp_learning_curve_90ep.png")
|
|
|
|
|
| if hasattr(weighted_mlp, 'validation_scores_'):
|
| plt.figure(figsize=(12, 8))
|
| plt.plot(weighted_mlp.validation_scores_)
|
| plt.title('Weighted MLP Validation Score Curve - 90 Epoch Features')
|
| plt.xlabel('Iterations')
|
| plt.ylabel('Score')
|
| plt.grid(True)
|
| plt.savefig('weighted_mlp_validation_curve_90ep.png')
|
| print("Saved validation curve to weighted_mlp_validation_curve_90ep.png")
|
|
|
| print("\nDone! You can now use the trained weighted model for predictions.")
|
|
|
|
|
| def predict_gleason_grade_weighted(embedding_vector,
|
| model_path='tcga_gleason_weighted_mlp_model_90ep.joblib',
|
| scaler_path='tcga_gleason_weighted_mlp_scaler_90ep.joblib',
|
| encoder_path='tcga_gleason_weighted_label_encoder_90ep.joblib',
|
| weights_path='tcga_gleason_class_weights_90ep.joblib'):
|
| """Predict Gleason grade using weighted model with class weights information"""
|
| model = joblib.load(model_path)
|
| scaler = joblib.load(scaler_path)
|
| label_encoder = joblib.load(encoder_path)
|
| class_weights = joblib.load(weights_path)
|
|
|
|
|
| embedding_vector = np.array(embedding_vector).reshape(1, -1)
|
| embedding_vector_scaled = scaler.transform(embedding_vector)
|
|
|
|
|
| prediction = model.predict(embedding_vector_scaled)
|
| probabilities = model.predict_proba(embedding_vector_scaled)
|
|
|
|
|
| prediction_original = label_encoder.inverse_transform(prediction)
|
|
|
|
|
| predicted_class_id = prediction[0]
|
| predicted_class_weight = class_weights.get(predicted_class_id, 1.0)
|
|
|
| return {
|
| 'predicted_grade': prediction_original[0],
|
| 'probabilities': dict(zip(label_encoder.classes_, probabilities[0])),
|
| 'class_weight': predicted_class_weight,
|
| 'confidence_score': np.max(probabilities[0])
|
| }
|
|
|
| def compare_models_performance():
|
| """Compare regular and weighted model performance"""
|
| print("=== MODEL PERFORMANCE COMPARISON ===")
|
|
|
|
|
| try:
|
| regular_model = joblib.load('tcga_gleason_mlp_model_90ep.joblib')
|
| weighted_model = joblib.load('tcga_gleason_weighted_mlp_model_90ep.joblib')
|
|
|
| print("Both models loaded successfully!")
|
| print(f"Regular model type: {type(regular_model)}")
|
| print(f"Weighted model type: {type(weighted_model)}")
|
|
|
|
|
| print(f"\nRegular model hidden layers: {regular_model.hidden_layer_sizes}")
|
| print(f"Weighted model hidden layers: {weighted_model.hidden_layer_sizes}")
|
|
|
|
|
| if hasattr(regular_model, 'loss_curve_') and hasattr(weighted_model, 'loss_curve_'):
|
| print(f"\nRegular model final loss: {regular_model.loss_curve_[-1]:.6f}")
|
| print(f"Weighted model final loss: {weighted_model.loss_curve_[-1]:.6f}")
|
|
|
|
|
| if hasattr(regular_model, 'validation_scores_') and hasattr(weighted_model, 'validation_scores_'):
|
| print(f"Regular model final validation score: {regular_model.validation_scores_[-1]:.6f}")
|
| print(f"Weighted model final validation score: {weighted_model.validation_scores_[-1]:.6f}")
|
|
|
| except FileNotFoundError as e:
|
| print(f"Model file not found: {e}")
|
| print("Please run the training first to generate both models.")
|
|
|
| if __name__ == "__main__":
|
| main()
|
|
|