public code v1

This commit is contained in:
2026-05-22 10:02:10 +02:00
commit 46a9ecf065
166 changed files with 6982454 additions and 0 deletions
+956
View File
@@ -0,0 +1,956 @@
import streamlit as st
import time
# Library Imports
from pygrex.models import (
ALS,
BPR,
ExplAutoencoderTorch,
EMFModel,
GMFModel,
MLPModel,
SVD,
KNNBasic,
)
from pygrex.evaluator import (
run_leave_one_out_evaluation,
run_evaluation_with_proper_split,
)
st.set_page_config(page_title="Model Training", page_icon="🧠", layout="wide")
st.title("🧠 Model Selection & Training")
# Check if data is loaded
if not st.session_state.get("data_loaded", False):
st.warning("⚠️ Please load data on the **📄 Data Preparation** page first.")
st.stop() # Stop execution if no data is loaded
# Model Selection
st.header("1. Select a Model")
# As you add more models to your library, you can add them to this list.
model_option = st.selectbox(
"Choose a recommendation model:",
("ALS", "BPR", "Autoencoder", "EMF", "GMF", "MLP", "KNN", "SVD"),
)
# Hyperparameter Configuration
st.header("2. Configure Hyperparameters")
model_params = {}
if model_option == "ALS":
st.subheader("ALS (Alternating Least Squares) Parameters")
# Create columns for a cleaner layout
col1, col2, col3 = st.columns(3)
with col1:
latent_dim = st.number_input(
"Latent Dimensions (factors)",
min_value=1,
max_value=500,
value=100,
step=10,
help="The number of latent factors to compute.",
)
with col2:
reg_term = st.number_input(
"Regularization Term",
min_value=0.001,
max_value=1.0,
value=0.001,
step=0.001,
format="%.3f",
help="The regularization factor.",
)
with col3:
epochs = st.number_input(
"Epochs (iterations)",
min_value=1,
max_value=200,
value=10,
step=5,
help="The number of ALS iterations.",
)
model_params = {
"latent_dim": latent_dim,
"reg_term": reg_term,
"epochs": epochs,
}
elif model_option == "BPR":
st.subheader("BPR (Bayesian Personalised Ranking) Parameters")
# First Row
col1_r1, col2_r1, col3_r1 = st.columns(3)
with col1_r1:
latent_dim = st.number_input(
"Latent Dimensions (factors)",
min_value=1,
max_value=500,
value=100,
step=10,
help="The number of latent factors to compute.",
)
with col2_r1:
reg_term = st.number_input(
"Regularization Term",
min_value=0.001,
max_value=1.0,
value=0.001,
step=0.001,
format="%.3f",
help="The regularization factor.",
)
with col3_r1:
epochs = st.number_input(
"Epochs (iterations)",
min_value=1,
max_value=200,
value=10,
step=5,
help="The number of ALS iterations.",
)
# Second Row
col1_r2, col2_r2, col3_r2 = st.columns(3)
with col1_r2:
learning_rate = st.number_input(
"Learning Rate",
min_value=0.0,
max_value=0.1,
value=0.01,
step=0.01,
format="%.2f",
help="The step size at each iteration while moving toward a minimum of the loss function.",
)
model_params = {
"latent_dim": latent_dim,
"reg_term": reg_term,
"epochs": epochs,
"learning_rate": learning_rate,
}
elif model_option == "Autoencoder":
st.subheader("Autoencoder Parameters")
# First Row
col1_r1, col2_r1, col3_r1 = st.columns(3)
with col1_r1:
learning_rate = st.number_input(
"Learning Rate",
min_value=0.0001,
max_value=0.1,
value=0.005,
step=0.001,
format="%.4f",
help="The step size at each iteration while moving toward a minimum of the loss function.",
)
with col2_r1:
weight_decay = st.number_input(
"Weight Decay",
min_value=0.0000001,
max_value=0.0001,
value=0.0000001,
step=0.0000001,
format="%.7f",
help="The regularization factor to prevent overfitting by penalizing large weights.",
)
with col3_r1:
hidden_layer_features = st.number_input(
"Hidden Layer Features",
min_value=4,
max_value=128,
value=8,
step=4,
help="The number of features in the hidden layers of the neural network.",
)
# Second Row
col1_r2, col2_r2, col3_r2 = st.columns(3)
with col1_r2:
epochs = st.number_input(
"Epochs (iterations)",
min_value=1,
max_value=200,
value=30,
step=5,
help="The number of complete passes through the entire training dataset.",
)
with col2_r2:
cuda = st.checkbox(
"Use CUDA (GPU)",
value=False,
help="Check to use NVIDIA CUDA for GPU acceleration if available.",
)
with col3_r2:
optimizer_name = st.selectbox(
"Optimizer",
options=["adam", "sgd", "rmsprop"],
index=0, # 'adam'
help="The optimization algorithm to use for training the model.",
)
# Third Row
col1_r3, col2_r3, col3_r3 = st.columns(3)
with col1_r3:
positive_threshold = st.number_input(
"Positive Threshold",
min_value=1,
max_value=5,
value=3,
step=1,
help="The minimum rating value considered as a 'positive' interaction.",
)
with col2_r3:
knn = st.number_input(
"K-Nearest Neighbors (KNN)",
min_value=1,
max_value=50,
value=10,
step=1,
help="The number of nearest neighbors to consider for KNN-based models.",
)
with col3_r3:
expl = st.checkbox(
"Enable Explanations",
value=True,
help="Check to enable model explanations or interpretability features.",
)
model_params = {
"learning_rate": learning_rate,
"weight_decay": weight_decay,
"hidden_layer_features": hidden_layer_features,
"epochs": epochs,
"cuda": cuda,
"optimizer_name": optimizer_name,
"positive_threshold": positive_threshold,
"knn": knn,
"expl": expl,
}
elif model_option == "EMF":
st.subheader("EMF (Explainable Matrix Factorisation) Parameters")
# First Row
col1_r1, col2_r1, col3_r1 = st.columns(3)
with col1_r1:
learning_rate = st.number_input(
"Learning Rate",
min_value=0.0001,
max_value=0.1,
value=0.01,
step=0.001,
format="%.4f",
help="The step size at each iteration for the EMF model.",
)
with col2_r1:
reg_term = st.number_input(
"Regularization Term",
min_value=0.0001,
max_value=1.0,
value=0.001,
step=0.001,
format="%.4f",
help="The regularization factor for the main matrix factorization components.",
)
with col3_r1:
expl_reg_term = st.number_input(
"Explanation Regularization Term",
min_value=0.0,
max_value=1.0,
value=0.0,
step=0.001,
format="%.4f",
help="The regularization factor for the explanation components in EMF.",
)
# Second Row
col1_r2, col2_r2, col3_r2 = st.columns(3)
with col1_r2:
latent_dim = st.number_input(
"Latent Dimension",
min_value=10,
max_value=200,
value=80,
step=10,
help="The number of latent factors used in the matrix factorization.",
)
with col2_r2:
epochs = st.number_input(
"Epochs (iterations)",
min_value=1,
max_value=200,
value=10,
step=5,
help="The number of complete passes through the entire training dataset for EMF.",
)
with col3_r2:
positive_threshold = st.number_input(
"Positive Threshold",
min_value=1,
max_value=5,
value=3,
step=1,
help="The minimum rating value considered as a 'positive' interaction for EMF.",
)
# Third Row
col1_r3, col2_r3, col3_r3 = st.columns(3)
with col1_r3:
knn = st.number_input(
"K-Nearest Neighbors (KNN)",
min_value=1,
max_value=50,
value=10,
step=1,
help="The number of nearest neighbors to consider for KNN-based aspects of EMF.",
)
model_params = {
"learning_rate": learning_rate,
"reg_term": reg_term,
"expl_reg_term": expl_reg_term,
"latent_dim": latent_dim,
"epochs": epochs,
"positive_threshold": positive_threshold,
"knn": knn,
}
elif model_option == "GMF":
st.subheader("GMF (Generalised Matrix Factorisation) Parameters")
# First Row
col1_r1, col2_r1, col3_r1 = st.columns(3)
with col1_r1:
learning_rate = st.number_input(
"Learning Rate",
min_value=0.0001,
max_value=0.1,
value=0.005,
step=0.001,
format="%.4f",
help="The step size at each iteration for the GMF model.",
)
with col2_r1:
weight_decay = st.number_input(
"Weight Decay",
min_value=0.0000001,
max_value=0.0001,
value=0.0000001,
step=0.0000001,
format="%.7f",
help="The regularization factor to prevent overfitting in GMF.",
)
with col3_r1:
latent_dim = st.number_input(
"Latent Dimension",
min_value=4,
max_value=128,
value=8,
step=4,
help="The number of latent factors for users and items in GMF.",
)
# Second Row
col1_r2, col2_r2, col3_r2 = st.columns(3)
with col1_r2:
epochs = st.number_input(
"Epochs (iterations)",
min_value=1,
max_value=200,
value=30,
step=5,
help="The number of complete passes through the training data for GMF.",
)
with col2_r2:
num_negative = st.number_input(
"Number of Negative Samples",
min_value=1,
max_value=100,
value=10,
step=1,
help="The number of negative samples per positive interaction during training.",
)
with col3_r2:
batch_size = st.number_input(
"Batch Size",
min_value=64,
max_value=4096,
value=1024,
step=64,
help="The number of samples per gradient update.",
)
# Third Row
col1_r3, col2_r3, col3_r3 = st.columns(3)
with col1_r3:
cuda = st.checkbox(
"Use CUDA (GPU)",
value=False,
help="Check to use NVIDIA CUDA for GPU acceleration if available for GMF.",
)
with col2_r3:
optimizer_name = st.selectbox(
"Optimizer",
options=["adam", "sgd", "rmsprop"],
index=0, # 'adam'
help="The optimization algorithm to use for training the GMF model.",
)
# col3_r3 is left empty here if no further parameters for GMF
model_params = {
"learning_rate": learning_rate,
"weight_decay": weight_decay,
"latent_dim": latent_dim,
"epochs": epochs,
"num_negative": num_negative,
"batch_size": batch_size,
"cuda": cuda,
"optimizer_name": optimizer_name,
}
elif model_option == "MLP":
st.subheader("MLP (Multi-Layer Perceptron) Parameters")
# First Row
col1_r1, col2_r1, col3_r1 = st.columns(3)
with col1_r1:
learning_rate = st.number_input(
"Learning Rate",
min_value=0.0001,
max_value=0.1,
value=0.005,
step=0.001,
format="%.4f",
help="The step size at each iteration for the MLP model.",
)
with col2_r1:
weight_decay = st.number_input(
"Weight Decay",
min_value=0.0000001,
max_value=0.0001,
value=0.0000001,
step=0.0000001,
format="%.7f",
help="The regularization factor to prevent overfitting in MLP.",
)
with col3_r1:
latent_dim = st.number_input(
"Latent Dimension",
min_value=4,
max_value=128,
value=8,
step=4,
help="The number of latent factors for users and items in MLP.",
)
# Second Row
col1_r2, col2_r2, col3_r2 = st.columns(3)
with col1_r2:
epochs = st.number_input(
"Epochs (iterations)",
min_value=1,
max_value=200,
value=30,
step=5,
help="The number of complete passes through the training data for MLP.",
)
with col2_r2:
num_negative = st.number_input(
"Number of Negative Samples",
min_value=1,
max_value=100,
value=10,
step=1,
help="The number of negative samples per positive interaction during MLP training.",
)
with col3_r2:
batch_size = st.number_input(
"Batch Size",
min_value=64,
max_value=4096,
value=1024,
step=64,
help="The number of samples per gradient update for MLP.",
)
# Third Row
col1_r3, col2_r3, col3_r3 = st.columns(3)
with col1_r3:
cuda = st.checkbox(
"Use CUDA (GPU)",
value=False,
help="Check to use NVIDIA CUDA for GPU acceleration if available for MLP.",
)
with col2_r3:
optimizer_name = st.selectbox(
"Optimizer",
options=["adam", "sgd", "rmsprop"],
index=0, # 'adam'
help="The optimization algorithm to use for training the MLP model.",
)
# col3_r3 is left empty here if no further parameters for MLP
model_params = {
"learning_rate": learning_rate,
"weight_decay": weight_decay,
"latent_dim": latent_dim,
"epochs": epochs,
"num_negative": num_negative,
"batch_size": batch_size,
"cuda": cuda,
"optimizer_name": optimizer_name,
}
elif model_option == "KNN":
st.subheader("KNN (K-Nearest Neighbors) Parameters")
# First Row
col1_r1, col2_r1, col3_r1 = st.columns(3)
with col1_r1:
k_neighbors = st.number_input(
"Number of Neighbors (k)",
min_value=1,
max_value=100,
value=50,
step=1,
help="The number of nearest neighbors to consider for making predictions.",
)
with col2_r1:
min_k_neighbors = st.number_input(
"Minimum Number of Neighbors",
min_value=1,
max_value=20,
value=3,
step=1,
help="The minimum number of neighbors required to make a prediction.",
)
with col3_r1:
similarity_type = st.selectbox(
"Similarity Metric",
options=["cosine", "pearson"],
index=1, # 'pearson'
help="The similarity metric to use for finding nearest neighbors.",
)
# Second Row
col1_r2, col2_r2, col3_r2 = st.columns(3)
with col1_r2:
boolean_user_based = st.checkbox(
"User-Based Collaborative Filtering",
value=True,
help="Check to use user-based collaborative filtering; uncheck for item-based.",
)
# col2_r2 and col3_r2 is left empty here if no further parameters for KNN
model_params = {
"k_neighbors": k_neighbors,
"min_k_neighbors": min_k_neighbors,
"similarity_type": similarity_type,
"boolean_user_based": boolean_user_based,
}
elif model_option == "SVD":
st.subheader("SVD Parameters")
N_FACTORS = 64
N_EPOCHS = 30
LEARNING_RATE = 0.005
REGULARIZATION = 0.08
RANDOM_STATE = 42
# First Row
col1_r1, col2_r1, col3_r1 = st.columns(3)
with col1_r1:
n_factors = st.number_input(
"Latent Dimensions (factors)",
min_value=1,
max_value=100,
value=64,
step=1,
help="The number of latent factors to compute.",
)
with col2_r1:
n_epochs = st.number_input(
"Epochs (iterations)",
min_value=1,
max_value=35,
value=30,
step=1,
help="The number of model iterations.",
)
with col3_r1:
learning_rater = st.number_input(
"Learning Rate",
min_value=0.001,
max_value=0.050,
value=0.005,
help="The step size at each iteration while moving toward a minimum of the loss function.",
)
# Second Row
col1_r2, col2_r2, col3_r2 = st.columns(3)
with col1_r2:
early_stopping = st.checkbox(
"Enable Early Stopping",
value=False,
help="Check to stop training when validation performance degrades.",
)
with col2_r2:
reg = st.number_input(
"Regularization Term",
min_value=0.01,
max_value=0.50,
value=0.08,
step=0.01,
format="%.3f",
help="The regularization factor.",
)
with col3_r2:
init_mean = st.number_input(
"Initialization Mean",
min_value=0.0,
max_value=0.5,
value=0.0,
step=0.01,
format="%.2f",
help="The mean for initializing latent factors.",
)
# Third Row
col1_r3, col2_r3, col3_r3 = st.columns(3)
with col1_r3:
init_std = st.number_input(
"Initialization Standard Deviation",
min_value=0.00,
max_value=0.5,
value=0.0,
step=0.01,
format="%.2f",
help="The standard deviation for initializing latent factors.",
)
with col2_r3:
random_state = st.number_input(
"Random State (Seed)",
min_value=1,
max_value=100,
value=42,
step=1,
help="The seed for random number generation to ensure reproducibility.",
)
# col3_r3 is left empty here if no further parameters for SVD
model_params = {
"n_factors": n_factors,
"n_epochs": n_epochs,
"learning_rater": learning_rater,
"reg": reg,
"init_mean": init_mean,
"init_std": init_std,
"random_state": random_state,
"early_stopping": early_stopping,
}
else:
st.info(f"Configuration for **{model_option}** is not yet implemented.")
st.stop()
# Model Training
st.header("3. Train the Model")
if st.button("Train Model", type="primary"):
with st.spinner(f"Training **{model_option}** model... This may take a moment."):
try:
# Retrieve the data_reader object from session state
data_reader = st.session_state.data_reader
model = None
# 1. Instantiate the model with user-defined hyperparameters
if model_option == "ALS":
model = ALS(**model_params)
elif model_option == "BPR":
model = BPR(**model_params)
elif model_option == "Autoencoder":
autoencoder_params = {
k: v
for k, v in model_params.items()
if k not in ["num_users", "num_items"]
}
model = ExplAutoencoderTorch(**autoencoder_params)
elif model_option == "EMF":
emf_params = {
k: v
for k, v in model_params.items()
if k not in ["num_users", "num_items"]
}
model = EMFModel(**emf_params)
elif model_option == "GMF":
gmf_params = {
k: v
for k, v in model_params.items()
if k not in ["num_users", "num_items"]
}
model = GMFModel(**gmf_params)
elif model_option == "MLP":
mlp_params = {
k: v
for k, v in model_params.items()
if k not in ["num_users", "num_items"]
}
model = MLPModel(**mlp_params)
elif model_option == "KNN":
if "k_neighbors" in model_params:
model_params["k"] = model_params.pop("k_neighbors")
knn_params = {
k: v
for k, v in model_params.items()
if k not in ["num_users", "num_items"]
}
model = KNNBasic(**knn_params)
elif model_option == "SVD":
if "learning_rater" in model_params:
model_params["lr"] = model_params.pop("learning_rater")
svd_params = {
k: v
for k, v in model_params.items()
if k not in ["num_users", "num_items"]
}
model = SVD(**svd_params)
if model:
start_time = time.time()
# 2. Fit the model using the processed dataset
model.fit(data_reader)
end_time = time.time()
training_time = end_time - start_time
# 3. Store the trained model in session state for the next page
st.session_state.trained_model = model
st.session_state.model_name = model_option
st.success(
f"✅ **{model_option}** model trained successfully in {training_time:.2f} seconds!"
)
except Exception as e:
st.error(f"An error occurred during model training: {e}")
if "trained_model" in st.session_state:
del st.session_state.trained_model
if "trained_model" in st.session_state:
st.markdown("")
st.header("4. Offline Model Evaluation")
with st.expander("🔬 Run Model Evaluation", expanded=True):
st.markdown("""
Choose your evaluation method:
- **Leave-One-Out**: More thorough but slower (recommended for final evaluation)
- **Train/Test Split**: Faster and practical for iterative testing
**Metrics Explained:**
- **Hit Ratio @10**: Percentage of users for whom we found at least one relevant item in top-10
- **NDCG @10**: Measures ranking quality - higher values mean better ranking of relevant items
""")
# Evaluation method selection
eval_method = st.radio(
"Select Evaluation Method:",
["Train/Test Split (Fast)", "Leave-One-Out (Thorough)"],
index=0,
)
# Parameters
col1, col2 = st.columns(2)
with col1:
test_size = 0.2 # Default value
if eval_method == "Train/Test Split (Fast)":
test_size = st.slider("Test Set Size (%)", 10, 30, 20) / 100
eval_top_n = st.number_input("Top-N for evaluation", 1, 20, 10)
with col2:
if eval_method == "Leave-One-Out (Thorough)":
st.info("Leave-one-out will use 1 item per user for testing")
# Run evaluation button
eval_button_key = f"run_eval_{eval_method.replace(' ', '_').replace('(', '').replace(')', '')}"
if st.button("Run Evaluation", key=eval_button_key, type="primary"):
with st.spinner(
f"Running {eval_method.lower()} evaluation... Please wait."
):
try:
# Get the model configuration for re-instantiation
model_name = st.session_state.model_name
data_reader = st.session_state.data_reader
# Re-instantiate model with same parameters
if model_option == "ALS":
eval_model = ALS(**model_params)
elif model_option == "BPR":
eval_model = BPR(**model_params)
elif model_option == "Autoencoder":
autoencoder_params = {
k: v
for k, v in model_params.items()
if k not in ["num_users", "num_items"]
}
eval_model = ExplAutoencoderTorch(**autoencoder_params)
elif model_option == "EMF":
emf_params = {
k: v
for k, v in model_params.items()
if k not in ["num_users", "num_items"]
}
eval_model = EMFModel(**emf_params)
elif model_option == "GMF":
gmf_params = {
k: v
for k, v in model_params.items()
if k not in ["num_users", "num_items"]
}
eval_model = GMFModel(**gmf_params)
elif model_option == "MLP":
mlp_params = {
k: v
for k, v in model_params.items()
if k not in ["num_users", "num_items"]
}
eval_model = MLPModel(**mlp_params)
elif model_option == "KNN":
if "k_neighbors" in model_params:
model_params["k"] = model_params.pop("k_neighbors")
knn_params = {
k: v
for k, v in model_params.items()
if k not in ["num_users", "num_items"]
}
eval_model = KNNBasic(**knn_params)
elif model_option == "SVD":
if "learning_rater" in model_params:
model_params["lr"] = model_params.pop("learning_rater")
svd_params = {
k: v
for k, v in model_params.items()
if k not in ["num_users", "num_items"]
}
eval_model = SVD(**svd_params)
else:
st.error(f"Evaluation not implemented for {model_name}")
st.stop()
# Run the appropriate evaluation
if eval_method == "Leave-One-Out (Thorough)":
evaluation_scores = run_leave_one_out_evaluation(
data_reader=data_reader,
model=eval_model,
top_n=eval_top_n,
)
else: # Train/Test Split
evaluation_scores = run_evaluation_with_proper_split(
data_reader=data_reader,
model=eval_model,
test_size=test_size,
top_n=eval_top_n,
)
# Store results
st.session_state.evaluation_scores = evaluation_scores
st.session_state.eval_method = eval_method
except Exception as e:
st.error(f"Evaluation failed: {str(e)}")
st.exception(e)
# Display results if available
if "evaluation_scores" in st.session_state:
st.markdown("")
st.subheader("📊 Evaluation Results")
scores = st.session_state.evaluation_scores
method = st.session_state.get("eval_method", "")
# Metrics display
col1, col2, col3 = st.columns(3)
with col1:
st.metric(
label=f"Hit Ratio @{eval_top_n}",
value=f"{scores.get('Hit Ratio', 0.0):.2%}",
help="Percentage of test users for whom at least one relevant item was found in top-10",
)
with col2:
ndcg_value = scores.get("NDCG", scores.get("eNDCG", 0.0))
st.metric(
label=f"NDCG @{eval_top_n}",
value=f"{ndcg_value:.4f}",
help="Normalized Discounted Cumulative Gain - measures ranking quality",
)
with col3:
st.metric(
label="Evaluation Time",
value=f"{scores.get('evaluation_time', 0):.1f}s",
help="Time taken to complete the evaluation",
)
# Additional info
if "test_interactions" in scores:
st.info(
f"📈 Evaluated on {scores['test_interactions']:,} test interactions using {method}"
)
# Performance interpretation
hit_ratio = scores.get("Hit Ratio", 0.0)
ndcg = ndcg_value
st.markdown("### 🎯 Performance Interpretation")
if hit_ratio > 0.15 and ndcg > 0.08:
st.success(
"🎉 Excellent performance! Your model shows strong recommendation capability."
)
elif hit_ratio > 0.08 and ndcg > 0.04:
st.success("✅ Good performance! Your model is working well.")
elif hit_ratio > 0.03 and ndcg > 0.02:
st.warning(
"⚠️ Moderate performance. Consider tuning hyperparameters or trying a different model."
)
else:
st.error(
"❌ Poor performance. The model may need significant improvements."
)
st.info("Navigate to the **🎯 Group Recommendation** page to continue.")