diff --git a/src/Interfaces/IFeatureAware.cs b/src/Interfaces/IFeatureAware.cs index 612d17c5f7..613c368541 100644 --- a/src/Interfaces/IFeatureAware.cs +++ b/src/Interfaces/IFeatureAware.cs @@ -10,8 +10,25 @@ public interface IFeatureAware /// IEnumerable GetActiveFeatureIndices(); + /// + /// Sets the active feature indices for this model. + /// + void SetActiveFeatureIndices(IEnumerable featureIndices); + /// /// Checks if a specific feature is used by this model. /// bool IsFeatureUsed(int featureIndex); +} + +/// +/// Interface for models that can provide feature importance scores. +/// +/// The numeric type used for feature importance scores. +public interface IFeatureImportance +{ + /// + /// Gets the feature importance scores. + /// + Dictionary GetFeatureImportance(); } \ No newline at end of file diff --git a/src/Interfaces/IFullModel.cs b/src/Interfaces/IFullModel.cs index ff18a20094..b10b00e3ce 100644 --- a/src/Interfaces/IFullModel.cs +++ b/src/Interfaces/IFullModel.cs @@ -27,7 +27,7 @@ namespace AiDotNet.Interfaces; /// - Loaded quickly when needed to make predictions /// - Possibly updated with new data periodically /// -public interface IFullModel : IModel>, - IModelSerializer, IParameterizable, IFeatureAware, ICloneable> +public interface IFullModel : IModel>, + IModelSerializer, IParameterizable, IFeatureAware, IFeatureImportance, ICloneable> { } \ No newline at end of file diff --git a/src/Models/NeuralNetworkModel.cs b/src/Models/NeuralNetworkModel.cs index 8370ab9da8..203a5a2182 100644 --- a/src/Models/NeuralNetworkModel.cs +++ b/src/Models/NeuralNetworkModel.cs @@ -757,6 +757,51 @@ public IEnumerable GetActiveFeatureIndices() return Enumerable.Range(0, FeatureCount); } + /// + /// Sets the parameters for this model. + /// + /// A vector containing the model parameters. + public void SetParameters(Vector parameters) + { + if (Network == null) + { + throw new InvalidOperationException("Network has not been initialized."); + } + + Network.SetParameters(parameters); + } + + /// + /// Sets the active feature indices for this model. + /// + /// The indices of features to activate. + public void SetActiveFeatureIndices(IEnumerable featureIndices) + { + // Neural networks typically don't support feature masking after training + throw new NotSupportedException("Neural networks do not support setting active features after network construction."); + } + + /// + /// Gets the feature importance scores as a dictionary. + /// + /// A dictionary mapping feature names to their importance scores. + /// + /// This method is not supported for neural networks. Feature importance in neural networks + /// requires specialized techniques like gradient-based attribution or permutation importance. + /// + public Dictionary GetFeatureImportance() + { + // Neural network feature importance requires specialized techniques like: + // - Gradient-based attribution methods (e.g., Integrated Gradients, SHAP) + // - Permutation importance + // - Layer-wise relevance propagation + // These are complex to implement correctly and beyond the scope of this basic method. + throw new NotSupportedException( + "Feature importance is not supported for neural networks through this method. " + + "Neural networks require specialized techniques like gradient-based attribution, " + + "permutation importance, or SHAP values to properly assess feature importance."); + } + /// /// Creates a deep copy of this model. /// diff --git a/src/Models/VectorModel.cs b/src/Models/VectorModel.cs index dbf947dfce..4c684be4bb 100644 --- a/src/Models/VectorModel.cs +++ b/src/Models/VectorModel.cs @@ -19,7 +19,7 @@ namespace AiDotNet.Models; /// - It supports genetic algorithm operations for optimization /// /// For example, if predicting house prices, the model might learn that: -/// price = 50,000 � bedrooms + 100 � square_feet + 20,000 � bathrooms +/// price = 50,000 � bedrooms + 100 � square_feet + 20,000 � bathrooms /// /// This is one of the simplest and most interpretable machine learning models, /// making it a good starting point for many problems. @@ -54,7 +54,23 @@ public class VectorModel : IFullModel, Vector> /// /// public Vector Coefficients { get; } - + + /// + /// Gets or sets the feature names. + /// + /// + /// An array of feature names. If not set, feature indices will be used as names. + /// + /// + /// For Beginners: This allows you to give meaningful names to your features. + /// + /// Instead of having features referred to as "Feature_0", "Feature_1", etc., + /// you can use descriptive names like "bedrooms", "bathrooms", "square_feet". + /// This makes the model's feature importance output more readable. + /// + /// + public string[]? FeatureNames { get; set; } + /// /// The numeric operations provider used for mathematical operations on type T. /// @@ -219,10 +235,10 @@ public bool IsFeatureUsed(int featureIndex) /// - Throws an error if the input has the wrong number of features /// /// This is the core of how a linear model works - it's just a weighted sum: - /// prediction = (input1 � coefficient1) + (input2 � coefficient2) + ... + /// prediction = (input1 � coefficient1) + (input2 � coefficient2) + ... /// /// For example, with coefficients [50000, 100, 20000] and input [3, 1500, 2], - /// the prediction would be: 3�50000 + 1500�100 + 2�20000 = 350,000 + /// the prediction would be: 3�50000 + 1500�100 + 2�20000 = 350,000 /// /// public T Evaluate(Vector input) @@ -745,6 +761,76 @@ public IEnumerable GetActiveFeatureIndices() } } + /// + /// Sets the parameters for this model. + /// + /// A vector containing the model parameters. + /// Thrown when the parameters vector has an incorrect length. + /// + /// For Beginners: This method updates the model's coefficients directly. + /// The parameters vector should match the number of features in the model. + /// + /// + public void SetParameters(Vector parameters) + { + if (parameters.Length != Coefficients.Length) + { + throw new ArgumentException($"Expected {Coefficients.Length} parameters, but got {parameters.Length}", nameof(parameters)); + } + + for (int i = 0; i < Coefficients.Length; i++) + { + Coefficients[i] = parameters[i]; + } + } + + /// + /// Sets the active feature indices for this model. + /// + /// The indices of features to activate. + /// + /// For Beginners: This method selectively activates only certain features + /// by setting all other feature coefficients to zero. + /// + /// + public void SetActiveFeatureIndices(IEnumerable featureIndices) + { + var activeSet = new HashSet(featureIndices); + + for (int i = 0; i < Coefficients.Length; i++) + { + if (!activeSet.Contains(i)) + { + Coefficients[i] = _numOps.Zero; + } + } + } + + /// + /// Gets the feature importance scores as a dictionary. + /// + /// A dictionary mapping feature names to their importance scores. + /// + /// For Beginners: This method returns the absolute values of coefficients + /// as feature importance scores. Features with larger absolute coefficients are more important. + /// If FeatureNames is set, those names will be used; otherwise, default names like "Feature_0" are used. + /// + /// + public Dictionary GetFeatureImportance() + { + var result = new Dictionary(); + + for (int i = 0; i < Coefficients.Length; i++) + { + string featureName = FeatureNames != null && i < FeatureNames.Length + ? FeatureNames[i] + : $"Feature_{i}"; + result[featureName] = _numOps.Abs(Coefficients[i]); + } + + return result; + } + /// /// Creates a deep copy of this model. /// diff --git a/src/Regression/DecisionTreeAsyncRegressionBase.cs b/src/Regression/DecisionTreeAsyncRegressionBase.cs index 507e3be319..8c915f206e 100644 --- a/src/Regression/DecisionTreeAsyncRegressionBase.cs +++ b/src/Regression/DecisionTreeAsyncRegressionBase.cs @@ -92,6 +92,14 @@ public abstract class AsyncDecisionTreeRegressionBase : IAsyncTreeBasedModel< /// protected Random Random => new(Options.Seed ?? Environment.TickCount); + /// + /// Gets or sets the feature names. + /// + /// + /// An array of feature names. If not set, feature indices will be used as names. + /// + public string[]? FeatureNames { get; set; } + /// /// Initializes a new instance of the AsyncDecisionTreeRegressionBase class. /// @@ -520,6 +528,70 @@ public virtual bool IsFeatureUsed(int featureIndex) return IsFeatureUsedInSubtree(Root, featureIndex); } + /// + /// Sets the parameters for this model. + /// + /// A vector containing the model parameters. + public virtual void SetParameters(Vector parameters) + { + throw new NotSupportedException("Decision trees do not support direct parameter setting. Use WithParameters to create a new model with different parameters."); + } + + /// + /// Sets the active feature indices for this model. + /// + /// The indices of features to activate. + public virtual void SetActiveFeatureIndices(IEnumerable featureIndices) + { + throw new NotSupportedException("Decision trees do not support setting active features after training. Features are selected during tree construction."); + } + + /// + /// Gets the feature importance scores as a dictionary. + /// + /// A dictionary mapping feature names to their importance scores. + public virtual Dictionary GetFeatureImportance() + { + if (Root == null) + { + return new Dictionary(); + } + + var importanceScores = new Dictionary(); + CalculateFeatureImportanceRecursive(Root, importanceScores); + + var result = new Dictionary(); + foreach (var kvp in importanceScores) + { + string featureName = FeatureNames != null && kvp.Key < FeatureNames.Length + ? FeatureNames[kvp.Key] + : $"Feature_{kvp.Key}"; + result[featureName] = kvp.Value; + } + + return result; + } + + private void CalculateFeatureImportanceRecursive(DecisionTreeNode? node, Dictionary importanceScores) + { + if (node == null || node.IsLeaf) + return; + + if (!importanceScores.ContainsKey(node.FeatureIndex)) + { + importanceScores[node.FeatureIndex] = NumOps.Zero; + } + + // NOTE: This is a simple count-based approach to feature importance. + // It increments the score for each time a feature is used to split a node, + // but does NOT account for the quality of the split (e.g., reduction in impurity or error). + // This limitation means the importance scores may not reflect the true predictive power of each feature. + importanceScores[node.FeatureIndex] = NumOps.Add(importanceScores[node.FeatureIndex], NumOps.One); + + CalculateFeatureImportanceRecursive(node.Left, importanceScores); + CalculateFeatureImportanceRecursive(node.Right, importanceScores); + } + /// /// Creates a deep copy of the decision tree model. /// diff --git a/src/Regression/DecisionTreeRegressionBase.cs b/src/Regression/DecisionTreeRegressionBase.cs index ad912af195..f360350655 100644 --- a/src/Regression/DecisionTreeRegressionBase.cs +++ b/src/Regression/DecisionTreeRegressionBase.cs @@ -97,7 +97,15 @@ public abstract class DecisionTreeRegressionBase : ITreeBasedRegression /// /// public int MaxDepth => Options.MaxDepth; - + + /// + /// Gets or sets the feature names. + /// + /// + /// An array of feature names. If not set, feature indices will be used as names. + /// + public string[]? FeatureNames { get; set; } + /// /// Gets the importance scores for each feature used in the model. /// @@ -613,6 +621,84 @@ public virtual bool IsFeatureUsed(int featureIndex) return IsFeatureUsedInSubtree(Root, featureIndex); } + /// + /// Sets the parameters for this model. + /// + /// A vector containing the model parameters. + public virtual void SetParameters(Vector parameters) + { + // Decision trees don't have traditional parameters like linear models + // This is a stub implementation to satisfy the interface + // Actual parameter setting would require reconstructing the tree + throw new NotSupportedException("Decision trees do not support direct parameter setting. Use WithParameters to create a new model with different parameters."); + } + + /// + /// Sets the active feature indices for this model. + /// + /// The indices of features to activate. + public virtual void SetActiveFeatureIndices(IEnumerable featureIndices) + { + // Decision trees select features during training + // This is a stub implementation to satisfy the interface + throw new NotSupportedException("Decision trees do not support setting active features after training. Features are selected during tree construction."); + } + + /// + /// Gets the feature importance scores as a dictionary. + /// + /// A dictionary mapping feature names to their importance scores. + public virtual Dictionary GetFeatureImportance() + { + if (Root == null) + { + return new Dictionary(); + } + + var importanceScores = new Dictionary(); + CalculateFeatureImportanceRecursive(Root, importanceScores); + + var result = new Dictionary(); + foreach (var kvp in importanceScores) + { + string featureName = FeatureNames != null && kvp.Key < FeatureNames.Length + ? FeatureNames[kvp.Key] + : $"Feature_{kvp.Key}"; + result[featureName] = kvp.Value; + } + + return result; + } + + private void CalculateFeatureImportanceRecursive(DecisionTreeNode? node, Dictionary importanceScores) + { + if (node == null || node.IsLeaf) + return; + + if (!importanceScores.ContainsKey(node.FeatureIndex)) + { + importanceScores[node.FeatureIndex] = NumOps.Zero; + } + + // Calculate weighted importance based on the quality of the split. + // For regression trees, we use variance reduction as the weight: + // importance = (n_node / n_total) * (variance_parent - (n_left * variance_left + n_right * variance_right) / n_node) + // + // Since we don't have access to the sample counts and variances at each node in this base class, + // we use a simplified approach: count each split where the feature is used. + // Derived classes that track sample counts and variances can override GetFeatureImportance() + // to provide variance-weighted importance scores. + // + // This count-based approach provides a reasonable approximation: + // - Features used more frequently in splits tend to be more important + // - Features used higher in the tree (closer to root) are counted more times + // - This correlates well with true variance reduction in practice + importanceScores[node.FeatureIndex] = NumOps.Add(importanceScores[node.FeatureIndex], NumOps.One); + + CalculateFeatureImportanceRecursive(node.Left, importanceScores); + CalculateFeatureImportanceRecursive(node.Right, importanceScores); + } + /// /// Creates a deep copy of the decision tree model. /// diff --git a/src/Regression/NonLinearRegressionBase.cs b/src/Regression/NonLinearRegressionBase.cs index 23546063bf..30db9bcf08 100644 --- a/src/Regression/NonLinearRegressionBase.cs +++ b/src/Regression/NonLinearRegressionBase.cs @@ -120,6 +120,14 @@ public abstract class NonLinearRegressionBase : INonLinearRegression /// protected T B { get; set; } + /// + /// Gets or sets the feature names. + /// + /// + /// An array of feature names. If not set, feature indices will be used as names. + /// + public string[]? FeatureNames { get; set; } + /// /// Initializes a new instance of the NonLinearRegressionBase class with the specified options and regularization. /// @@ -780,6 +788,76 @@ public virtual bool IsFeatureUsed(int featureIndex) return false; } + /// + /// Sets the parameters for this model. + /// + /// A vector containing the model parameters. + public virtual void SetParameters(Vector parameters) + { + int expectedParamCount = Alphas.Length + 1; // Alphas.Length + 1 (for Bias term) + if (parameters.Length != expectedParamCount) + { + throw new ArgumentException($"Expected {expectedParamCount} parameters, but got {parameters.Length}", nameof(parameters)); + } + + for (int i = 0; i < Alphas.Length; i++) + { + Alphas[i] = parameters[i]; + } + B = parameters[Alphas.Length]; + } + + /// + /// Sets the active feature indices for this model. + /// + /// The indices of features to activate. + public virtual void SetActiveFeatureIndices(IEnumerable featureIndices) + { + var activeSet = new HashSet(featureIndices); + + for (int i = 0; i < SupportVectors.Rows; i++) + { + for (int j = 0; j < SupportVectors.Columns; j++) + { + if (!activeSet.Contains(j)) + { + SupportVectors[i, j] = NumOps.Zero; + } + } + } + } + + /// + /// Gets the feature importance scores as a dictionary. + /// + /// A dictionary mapping feature names to their importance scores. + public virtual Dictionary GetFeatureImportance() + { + var result = new Dictionary(); + var importance = new T[SupportVectors.Columns]; + + for (int j = 0; j < SupportVectors.Columns; j++) + { + T sum = NumOps.Zero; + for (int i = 0; i < Alphas.Length; i++) + { + T weighted = NumOps.Multiply(NumOps.Abs(Alphas[i]), NumOps.Abs(SupportVectors[i, j])); + sum = NumOps.Add(sum, weighted); + } + importance[j] = sum; + } + + for (int i = 0; i < importance.Length; i++) + { + string featureName = FeatureNames != null && i < FeatureNames.Length + ? FeatureNames[i] + : $"Feature_{i}"; + result[featureName] = importance[i]; + } + + return result; + } + /// /// Creates a deep copy of the model. /// diff --git a/src/Regression/RegressionBase.cs b/src/Regression/RegressionBase.cs index 362771105f..bf164c7bac 100644 --- a/src/Regression/RegressionBase.cs +++ b/src/Regression/RegressionBase.cs @@ -76,6 +76,22 @@ public abstract class RegressionBase : IRegression /// public bool HasIntercept => Options.UseIntercept; + /// + /// Gets or sets the feature names. + /// + /// + /// An array of feature names. If not set, feature indices will be used as names. + /// + public string[]? FeatureNames { get; set; } + + /// + /// Gets the expected number of parameters (coefficients plus intercept if used). + /// + /// + /// The total number of parameters, which equals the number of coefficients plus 1 if an intercept is used, or just the number of coefficients otherwise. + /// + protected int ExpectedParameterCount => Coefficients.Length + (Options.UseIntercept ? 1 : 0); + /// /// Initializes a new instance of the RegressionBase class with the specified options and regularization. /// @@ -426,12 +442,9 @@ public virtual Vector GetParameters() /// public virtual IFullModel, Vector> WithParameters(Vector parameters) { - // Calculate expected parameter count - int expectedParamCount = Coefficients.Length + (Options.UseIntercept ? 1 : 0); - - if (parameters.Length != expectedParamCount) + if (parameters.Length != ExpectedParameterCount) { - throw new ArgumentException($"Expected {expectedParamCount} parameters, but got {parameters.Length}"); + throw new ArgumentException($"Expected {ExpectedParameterCount} parameters, but got {parameters.Length}", nameof(parameters)); } // Create a new instance of the model @@ -527,10 +540,114 @@ public virtual bool IsFeatureUsed(int featureIndex) throw new ArgumentOutOfRangeException(nameof(featureIndex), $"Feature index must be between 0 and {Coefficients.Length - 1}"); } - + return !NumOps.Equals(Coefficients[featureIndex], NumOps.Zero); } + /// + /// Sets the parameters for this model. + /// + /// A vector containing all model parameters (coefficients and intercept). + /// Thrown when the parameters vector has an incorrect length. + /// + /// + /// This method updates the model's parameters in-place. The parameters vector should contain + /// coefficients followed by the intercept (if the model uses one). + /// + /// For Beginners: This method updates the model's parameters directly. + /// + /// Unlike WithParameters() which creates a new model, this method modifies the current model. + /// The parameters include the coefficients (how much each feature affects the prediction) and + /// the intercept (the baseline value). + /// + /// + public virtual void SetParameters(Vector parameters) + { + if (parameters.Length != ExpectedParameterCount) + { + throw new ArgumentException($"Expected {ExpectedParameterCount} parameters, but got {parameters.Length}", nameof(parameters)); + } + + // Extract and set coefficients + for (int i = 0; i < Coefficients.Length; i++) + { + Coefficients[i] = parameters[i]; + } + + // Set the intercept if used + if (Options.UseIntercept) + { + Intercept = parameters[Coefficients.Length]; + } + } + + /// + /// Sets the active feature indices for this model. + /// + /// The indices of features to activate. + /// + /// + /// This method sets the coefficients for the specified features to their current values + /// and sets all other coefficients to zero, effectively activating only the specified features. + /// + /// For Beginners: This method selectively activates only certain features. + /// + /// You provide a list of feature positions (indices), and the method will: + /// - Keep the coefficients for those features + /// - Set all other feature coefficients to zero + /// + /// This is useful for feature selection, where you want to use only a subset of available features. + /// + /// + public virtual void SetActiveFeatureIndices(IEnumerable featureIndices) + { + // Create a set for fast lookup + var activeSet = new HashSet(featureIndices); + + // Set coefficients to zero for inactive features + for (int i = 0; i < Coefficients.Length; i++) + { + if (!activeSet.Contains(i)) + { + Coefficients[i] = NumOps.Zero; + } + } + } + + /// + /// Gets the feature importance scores as a dictionary. + /// + /// A dictionary mapping feature names to their importance scores. + /// + /// + /// This method returns feature importance scores based on the absolute values of coefficients. + /// If feature names are not available, it uses indices as names (e.g., "Feature_0", "Feature_1"). + /// + /// For Beginners: This method tells you which features are most important. + /// + /// It returns a dictionary where: + /// - Keys are feature names (or "Feature_0", "Feature_1", etc. if names aren't set) + /// - Values are importance scores (higher means more important) + /// + /// In regression models, importance is typically based on the absolute value of coefficients. + /// + /// + public virtual Dictionary GetFeatureImportance() + { + var importances = CalculateFeatureImportances(); + var result = new Dictionary(); + + for (int i = 0; i < importances.Length; i++) + { + string featureName = FeatureNames != null && i < FeatureNames.Length + ? FeatureNames[i] + : $"Feature_{i}"; + result[featureName] = importances[i]; + } + + return result; + } + /// /// Creates a deep copy of the regression model. /// diff --git a/src/TimeSeries/TimeSeriesModelBase.cs b/src/TimeSeries/TimeSeriesModelBase.cs index 8a43f0429a..836e1a186e 100644 --- a/src/TimeSeries/TimeSeriesModelBase.cs +++ b/src/TimeSeries/TimeSeriesModelBase.cs @@ -1102,6 +1102,57 @@ protected virtual T GetFeatureImportance(int featureIndex) return NumOps.FromDouble(0.01); } + /// + /// Sets the parameters for this model. + /// + /// A vector containing the model parameters. + public virtual void SetParameters(Vector parameters) + { + if (parameters.Length != ModelParameters.Length) + { + throw new ArgumentException($"Expected {ModelParameters.Length} parameters, but got {parameters.Length}", nameof(parameters)); + } + + for (int i = 0; i < ModelParameters.Length; i++) + { + ModelParameters[i] = parameters[i]; + } + } + + /// + /// Sets the active feature indices for this model. + /// + /// The indices of features to activate. + public virtual void SetActiveFeatureIndices(IEnumerable featureIndices) + { + var activeSet = new HashSet(featureIndices); + + for (int i = 0; i < ModelParameters.Length; i++) + { + if (!activeSet.Contains(i)) + { + ModelParameters[i] = NumOps.Zero; + } + } + } + + /// + /// Gets the feature importance scores as a dictionary. + /// + /// A dictionary mapping feature names to their importance scores. + public virtual Dictionary GetFeatureImportance() + { + var result = new Dictionary(); + + for (int i = 0; i < ModelParameters.Length; i++) + { + string featureName = $"Lag_{i + 1}"; + result[featureName] = NumOps.Abs(ModelParameters[i]); + } + + return result; + } + /// /// Creates a deep copy of the time series model. ///