Right now, I have a CNN model and a MLP model. Please adjust my CNN code such that it follows more closely to MLP in terms of using grid search, and making use of validation. So just change the model to CNN, but ensure the metrics used for optimizing is the same as MLP. Please make this results reproducible if run from another machine. Give me entire code.

My CNN Code now:
import os
import random
import numpy as np
import tensorflow as tf

# Define a seed value for reproducibility
seed = 42

# 1. Set the PYTHONHASHSEED environment variable for Python hash-based operations
os.environ['PYTHONHASHSEED'] = str(seed)

# 2. Set the Python built-in pseudo-random generator at a fixed value
random.seed(seed)

# 3. Set the NumPy pseudo-random generator at a fixed value
np.random.seed(seed)

# 4. Set the TensorFlow pseudo-random generator at a fixed value
tf.random.set_seed(seed)

# Import Required Packages


import pandas as pd

# For data preprocessing
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

# For performance metrics
from sklearn.metrics import (accuracy_score, precision_score, recall_score, 
                             f1_score, roc_auc_score, classification_report, 
                             confusion_matrix)

# For building the neural network
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import (Conv1D, MaxPooling1D, Flatten, Dense, Dropout, 
                                     BatchNormalization)
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.callbacks import EarlyStopping

# For hyperparameter tuning
import keras_tuner as kt
from keras_tuner.tuners import RandomSearch

# =============================================
# Version Information (Optional)
# =============================================

print(f'TensorFlow version: {tf.__version__}')
print(f'Keras Tuner version: {kt.__version__}')

# Data Loading and Preprocessing


# Read the dataset
data = pd.read_csv('dataset_phishing.csv')

# Define features and target variable
features = ['shortest_word_path', 'ratio_intMedia', 'links_in_tags',
            'nb_hyphens', 'page_rank', 'avg_word_path', 
            'ratio_extHyperlinks', 'longest_words_raw', 'google_index',
            'length_hostname', 'longest_word_host', 
            'domain_registration_length', 'nb_www', 'nb_underscore', 
            'nb_dots', 'ratio_extMedia', 'phish_hints', 'domain_in_title', 
            'web_traffic', 'safe_anchor', 'nb_space', 'shortening_service', 
            'ip', 'domain_age', 'nb_qm', 'nb_hyperlinks', 'nb_slash']

X = data[features]
y = data['status']  # 'status' is 'legitimate' or 'phishing'

# Map the 'status' to numerical labels
label_mapping = {'legitimate': 0, 'phishing': 1}
y = y.map(label_mapping)

# Check for any unmapped values or missing values
if y.isnull().any():
    print("Some 'status' values were not mapped properly. Please check the 'status' column for unexpected values.")
    print(y[y.isnull()])


# Handle missing values in features if any
X.fillna(0, inplace=True)

# Split into training and testing sets (80% train, 20% test)
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.20, random_state=seed, stratify=y)

# Standardize the features
scaler = StandardScaler()

X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

# Reshape data to be suitable for Conv1D (samples, timesteps, features)
# Since we have numerical features, we treat each feature as a "time step"
X_train_cnn = X_train_scaled.reshape(-1, X_train_scaled.shape[1], 1)
X_test_cnn = X_test_scaled.reshape(-1, X_test_scaled.shape[1], 1)


# Model Building and Hyperparameter Tuning


def build_model(hp):
    model = Sequential()
    
    # First Conv1D layer
    model.add(Conv1D(
        filters=hp.Int('filters_1', min_value=16, max_value=128, step=16),
        kernel_size=hp.Int('kernel_size_1', min_value=2, max_value=5, step=1),
        activation='relu',
        input_shape=(X_train_cnn.shape[1], 1),
        padding='same'
    ))
    model.add(BatchNormalization())
    model.add(MaxPooling1D(pool_size=2))
    model.add(Dropout(hp.Float('dropout_rate_1', min_value=0.0, max_value=0.5, step=0.1)))
    
    # Optional second Conv1D layer
    if hp.Boolean('second_conv_layer'):
        model.add(Conv1D(
            filters=hp.Int('filters_2', min_value=16, max_value=128, step=16),
            kernel_size=hp.Int('kernel_size_2', min_value=2, max_value=5, step=1),
            activation='relu',
            padding='same'
        ))
        model.add(BatchNormalization())
        model.add(MaxPooling1D(pool_size=2))
        model.add(Dropout(hp.Float('dropout_rate_2', min_value=0.0, max_value=0.5, step=0.1)))
    
    model.add(Flatten())
    
    # Dense layers
    for i in range(hp.Int('num_dense_layers', 1, 3)):
        model.add(Dense(
            units=hp.Int(f'units_{i}', min_value=32, max_value=256, step=32),
            activation='relu'
        ))
        model.add(BatchNormalization())
        model.add(Dropout(hp.Float(f'dropout_rate_dense_{i}', min_value=0.0, max_value=0.5, step=0.1)))
    
    # Output layer
    model.add(Dense(1, activation='sigmoid'))
    
    # Compile the model
    model.compile(
        optimizer=Adam(
            hp.Float('lr', min_value=1e-4, max_value=1e-2, sampling='LOG', default=1e-3)
        ),
        loss='binary_crossentropy',
        metrics=['accuracy']
    )
    return model

# Initialize the tuner
tuner = RandomSearch(
    build_model,
    objective='val_accuracy',
    max_trials=20,
    executions_per_trial=1,
    directory='tuning',
    project_name='cnn',
    seed=seed, 
    overwrite=True  
)

# Early stopping callback to prevent overfitting
early_stopping = EarlyStopping(monitor='val_loss', patience=5, restore_best_weights=True)

# Run the hyperparameter search
tuner.search(
    x=X_train_cnn,
    y=y_train,
    epochs=20,
    validation_split=0.2,
    callbacks=[early_stopping],
    verbose=1
)

# Get the optimal hyperparameters
best_hps = tuner.get_best_hyperparameters(num_trials=1)[0]
print(f"""
The hyperparameter search is complete. 
The optimal number of filters in the first Conv1D layer is {best_hps.get('filters_1')}, 
kernel size is {best_hps.get('kernel_size_1')}, 
and the learning rate is {best_hps.get('lr')}.
""")

# Build the model with the best hyperparameters
best_model = tuner.hypermodel.build(best_hps)

# Train the best model
history = best_model.fit(
    X_train_cnn, y_train,
    epochs=50,
    validation_split=0.2,
    callbacks=[early_stopping],
    verbose=1 
)


# Model Evaluation

# Evaluate the model on test data
test_loss, test_acc = best_model.evaluate(X_test_cnn, y_test)
print('Test Accuracy: {:.4f}'.format(test_acc))

# Predict probabilities and classes
y_pred_proba = best_model.predict(X_test_cnn).ravel()
y_pred = (y_pred_proba >= 0.5).astype(int)

# Calculate and print metrics
accuracy = accuracy_score(y_test, y_pred)
precision = precision_score(y_test, y_pred)
recall = recall_score(y_test, y_pred)
f1 = f1_score(y_test, y_pred)
roc_auc = roc_auc_score(y_test, y_pred_proba)

print('\nModel Performance Metrics:')
print('--------------------------')
print('Accuracy  : {:.4f}'.format(accuracy))
print('Precision : {:.4f}'.format(precision))
print('Recall    : {:.4f}'.format(recall))
print('F1 Score  : {:.4f}'.format(f1))
print('ROC AUC   : {:.4f}'.format(roc_auc))

print('\nConfusion Matrix:')
print(confusion_matrix(y_test, y_pred))

print('\nClassification Report:')
print(classification_report(y_test, y_pred))

My MLP implementation:
# Import packages
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.feature_selection import VarianceThreshold
import seaborn as sns
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LassoCV
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler, LabelEncoder
from sklearn.metrics import (accuracy_score, precision_score, recall_score, f1_score, roc_auc_score, classification_report, confusion_matrix)
# Read data
df = pd.read_csv('dataset_phishing.csv')
features = ['shortest_word_path',
 'ratio_intMedia',
 'links_in_tags',
 'nb_hyphens',
 'page_rank',
 'avg_word_path',
 'ratio_extHyperlinks',
 'longest_words_raw',
 'google_index',
 'length_hostname',
 'longest_word_host',
 'domain_registration_length',
 'nb_www',
 'nb_underscore',
 'nb_dots',
 'ratio_extMedia',
 'phish_hints',
 'domain_in_title',
 'web_traffic',
 'safe_anchor',
 'nb_space',
 'shortening_service',
 'ip',
 'domain_age',
 'nb_qm',
 'nb_hyperlinks',
 'nb_slash']
X = df[features]

scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

le = LabelEncoder()
y = le.fit_transform(df["status"])

# Step 1: Split data into 70% train and 30% temp (validation + test)
X_train, X_temp, y_train, y_temp = train_test_split(X_scaled, y, test_size=0.3, random_state=42, stratify=y)

# Step 2: Split the temp set into 50% validation and 50% test (15% each of the original data)
X_val, X_test, y_val, y_test = train_test_split(X_temp, y_temp, test_size=0.5, random_state=42)
from sklearn.neural_network import MLPClassifier
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, roc_auc_score, confusion_matrix, classification_report
from sklearn.model_selection import GridSearchCV
from sklearn.utils.class_weight import compute_class_weight
from imblearn.over_sampling import SMOTE
import seaborn as sns
import matplotlib.pyplot as plt

# Handle class imbalance by oversampling the minority class (phishing)
smote = SMOTE(random_state=42)
X_resampled, y_resampled = smote.fit_resample(X_train, y_train)

# Define the MLPClassifier with early stopping to avoid overfitting
mlp = MLPClassifier(random_state=42, early_stopping=True, validation_fraction=0.1)

# Set up hyperparameter grid for tuning
param_grid = {
    'hidden_layer_sizes': [(200, 100)],
    'activation': ['relu', 'tanh', 'logistic'],
    'solver': ['adam', 'sgd'],
    'alpha': [0.0001],
    'learning_rate': ['constant', 'adaptive'],
    'learning_rate_init': [0.008],
    # 'max_iter': [10000],
    # 'tol': [1e-4],
    # 'verbose': [True]
}

# Use GridSearchCV to find the best hyperparameters
grid_search = GridSearchCV(mlp, param_grid, cv=5, scoring='recall', n_jobs=-1, verbose=2)
grid_search.fit(X_resampled, y_resampled)

# Get the best model from the grid search
best_mlp = grid_search.best_estimator_

# Evaluate the model on the validation set
y_val_pred = best_mlp.predict(X_val)

# Calculate performance metrics for the validation set
val_accuracy = accuracy_score(y_val, y_val_pred)
val_precision = precision_score(y_val, y_val_pred)
val_recall = recall_score(y_val, y_val_pred)
val_f1 = f1_score(y_val, y_val_pred)
val_roc_auc = roc_auc_score(y_val, y_val_pred)

# Print evaluation metrics for the validation set
print("Validation Set Metrics:")
print(f"Accuracy: {val_accuracy:.4f}")
print(f"Precision: {val_precision:.4f}")
print(f"Recall: {val_recall:.4f}")
print(f"F1 Score: {val_f1:.4f}")
print(f"ROC AUC Score: {val_roc_auc:.4f}")

# Confusion Matrix for validation set
val_conf_matrix = confusion_matrix(y_val, y_val_pred)
sns.heatmap(val_conf_matrix, annot=True, fmt='d', cmap='Blues')
plt.title('Confusion Matrix (Validation Set)')
plt.xlabel('Predicted Label')
plt.ylabel('True Label')
plt.show()

# Classification Report for validation set
print("Classification Report (Validation Set):\n", classification_report(y_val, y_val_pred))

# Evaluate the model on the test set
y_pred = best_mlp.predict(X_test)

# Calculate performance metrics for the test set
accuracy = accuracy_score(y_test, y_pred)
precision = precision_score(y_test, y_pred)
recall = recall_score(y_test, y_pred)
f1 = f1_score(y_test, y_pred)
roc_auc = roc_auc_score(y_test, y_pred)

# Print the best hyperparameters and evaluation metrics for the test set
print("Test Set Metrics:")
print(f"Accuracy: {accuracy:.4f}")
print(f"Precision: {precision:.4f}")
print(f"Recall: {recall:.4f}")
print(f"F1 Score: {f1:.4f}")
print(f"ROC AUC Score: {roc_auc:.4f}")

# Confusion Matrix for test set
conf_matrix = confusion_matrix(y_test, y_pred)
sns.heatmap(conf_matrix, annot=True, fmt='d', cmap='Blues')
plt.title('Confusion Matrix (Test Set)')
plt.xlabel('Predicted Label')
plt.ylabel('True Label')
plt.show()

# Classification Report for test set
print("Classification Report (Test Set):\n", classification_report(y_test, y_pred))