Skip to content

Add Explainability to ModelBuilder #1031

Closed

Description

@chunky

System Information (please complete the following information):
$ mlnet --version
16.2.0+511cc1082bef7a4bbb2f83ad88c1c425c932079c
$ dotnet --version
3.1.402
$ lsb_release -d
Description: Ubuntu 19.10

Describe the bug
I've trained up a regression algorithm using AutoML on Linux, and am now trying to use Permutation Feature Importance, from the documentation. [URLs in "additional context", below]

Things that are problematic to try and follow:

  • Given that modelbuilder gave me a ModelInput, it's unclear how to use a ModelInput as the data: parameter. An elegant way to turn a ModelInput, or a collection of them, into an IDataView would be nice. Currently the only path I've found is to re-instantiate a fresh IDataView from my original CSV data using mlContext.Data.LoadFromFile [code that I lifted from the created ModelBuilder.cs]
  • Getting the predictionTransformer parameter is currently an awkward cast [which I was only able to figure out by opening a debugger and checking the types] followed by .LastTransformer. Presumably this isn't going to be copy-pastable to the next model, if modelbuilder chose a different best model.
  • I still haven't figured out how to map what PFI says, to the original parameters in the IDataView [or ModelInput]

Expected behavior
Personally I think the perfect product here would be one or more of a few things, any of which would improve my ability to access the functionality:

  • An additional .cs file in the wizard-created Sample that can do PFI on the created model [this would be ideal, and probably the least invasive change!]
  • Additional functions to instantiate a PermutationFeatureImportance that could take a TransformerChain instead of just the singular last transformer [current workflow also assumes that LastTransformer is even the important part of the chain]
  • A way to take an autocreated "ModelInput" and end with an IDataView.
  • A way to map ModelInput members to feature indices in the final weights/indices from PFI.

Additional context
https://docs.microsoft.com/en-us/dotnet/machine-learning/how-to-guides/explain-machine-learning-model-permutation-feature-importance-ml-net
and
https://docs.microsoft.com/en-us/dotnet/api/microsoft.ml.permutationfeatureimportanceextensions.permutationfeatureimportance?view=ml-dotnet#Microsoft_ML_PermutationFeatureImportanceExtensions_PermutationFeatureImportance__1_Microsoft_ML_RegressionCatalog_Microsoft_ML_ISingleFeaturePredictionTransformer___0__Microsoft_ML_IDataView_System_String_System_Boolean_System_Nullable_System_Int32__System_Int32_

Activity

  1. added
    GS TutorialBug from customer using Getting Started Tutorial
    on Oct 7, 2020
  2. LittleLittleCloud commented on Oct 7, 2020

    @LittleLittleCloud
    Member

    @JakeRadMSFT @briacht for broadcasting

  3. JakeRadMSFT commented on Oct 13, 2020

    @JakeRadMSFT
    Member

    @chunky I totally agree. I actually helped investigate adding PFI to Model Builder output (Still in progress). Let me share some code that might help!

  4. JakeRadMSFT commented on Oct 13, 2020

    @JakeRadMSFT
    Member

    I'll add some more notes to the code soon! Most of the code you need can be found in "CalculatePFI" function and "CreateModel"

    
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using Microsoft.ML;
    using Microsoft.ML.Data;
    using PfiSample.Model;
    using System.Collections.Immutable;
    
    namespace PfiSample.ConsoleApp
    {
        public static class ModelBuilder
        {
            private static string TRAIN_DATA_FILEPATH = @"C:\Users\anvelazq\Desktop\PfiSample\PfiSample.ConsoleApp\taxi-fare-train.csv";
            private static string MODEL_FILE = ConsumeModel.MLNetModelPath;
    
            // Create MLContext to be shared across the model creation workflow objects 
            // Set a random seed for repeatable/deterministic results across multiple trainings.
            private static MLContext mlContext = new MLContext(seed: 1);
    
            public static void CreateModel()
            {
                // Load Data
                IDataView trainingDataView = mlContext.Data.LoadFromTextFile<ModelInput>(
                                                path: TRAIN_DATA_FILEPATH,
                                                hasHeader: true,
                                                separatorChar: ',',
                                                allowQuoting: true,
                                                allowSparse: false);
    
                // Build training pipeline and Train Model
                IEstimator<ITransformer> trainingPipeline = BuildTrainingPipeline(mlContext);
                var mlModel = TrainModel(mlContext, trainingDataView, trainingPipeline);
    
                // Option 1: to extract predictor, requires to know the type in advance:
                // var predictor = ((TransformerChain<RegressionPredictionTransformer<LightGbmRegressionModelParameters>>)mlModel).LastTransformer;
    
                // Option 2: Should work always, as long as you _know_ the predictor is the last transformer in the chain.
                var predictor = ((IEnumerable<ITransformer>)mlModel).Last();
    
                // Option 3, need to load from disk the model first
                //var path = @"C:\Users\anvelazq\Desktop\PfiSample\model.zip";
                //mlContext.Model.Save(mlModel, trainingDataView.Schema, path);
                //var mlModel2 = mlContext.Model.Load(path, out var _);
                //var predictor = ((TransformerChain<ITransformer>) mlModel2).LastTransformer;
    
                // NOTE: Whatever option is chosen above, the explicit cast of the predictor seems to be needed here:
                CalculatePFI(mlContext, mlModel.Transform(trainingDataView), (ISingleFeaturePredictionTransformer<object>) predictor);
                
                // Evaluate quality of Model
                Evaluate(mlContext, trainingDataView, trainingPipeline);
    
                // Save model
                SaveModel(mlContext, mlModel, MODEL_FILE, trainingDataView.Schema);
            }
    
            public static IEstimator<ITransformer> BuildTrainingPipeline(MLContext mlContext)
            {
                // Data process configuration with pipeline data transformations 
                var dataProcessPipeline = mlContext.Transforms.Categorical.OneHotEncoding(new[] { new InputOutputColumnPair("vendor_id", "vendor_id"), new InputOutputColumnPair("payment_type", "payment_type") })
                                          .Append(mlContext.Transforms.Concatenate("Features", new[] { "vendor_id", "payment_type", "rate_code", "passenger_count", "trip_time_in_secs", "trip_distance" }));
                // Set the training algorithm 
                var trainer = mlContext.Regression.Trainers.LightGbm(labelColumnName: "fare_amount", featureColumnName: "Features");
    
                var trainingPipeline = dataProcessPipeline.Append(trainer);
    
                return trainingPipeline;
            }
    
            public static ITransformer TrainModel(MLContext mlContext, IDataView trainingDataView, IEstimator<ITransformer> trainingPipeline)
            {
                Console.WriteLine("=============== Training  model ===============");
    
                ITransformer model = trainingPipeline.Fit(trainingDataView);
    
                Console.WriteLine("=============== End of training process ===============");
                return model;
            }
    
            private static void Evaluate(MLContext mlContext, IDataView trainingDataView, IEstimator<ITransformer> trainingPipeline)
            {
                // Cross-Validate with single dataset (since we don't have two datasets, one for training and for evaluate)
                // in order to evaluate and get the model's accuracy metrics
                Console.WriteLine("=============== Cross-validating to get model's accuracy metrics ===============");
                var crossValidationResults = mlContext.Regression.CrossValidate(trainingDataView, trainingPipeline, numberOfFolds: 5, labelColumnName: "fare_amount");
                PrintRegressionFoldsAverageMetrics(crossValidationResults);
            }
    
            private static void CalculatePFI(MLContext mlContext, IDataView preprocessedTrainData, ISingleFeaturePredictionTransformer<object> model)
            {
                VBuffer<ReadOnlyMemory<char>> nameBuffer = default;
                preprocessedTrainData.Schema["Features"].Annotations.GetValue("SlotNames", ref nameBuffer); // NOTE: The column name "Features" needs to match the featureColumnName used in the trainer, the name "SlotNames" is always the same regardless of trainer.
                var featureColumnNames = nameBuffer.DenseValues().ToList();
    
                ImmutableArray<RegressionMetricsStatistics> permutationFeatureImportance =
                    mlContext
                    .Regression
                    .PermutationFeatureImportance(model, preprocessedTrainData, permutationCount: 1, labelColumnName: "fare_amount");
    
                var featureImportanceMetrics =
                    permutationFeatureImportance
                    .Select((metric, index) => new { index, metric.RSquared })
                    .OrderByDescending(myFeatures => Math.Abs(myFeatures.RSquared.Mean));
    
                Console.WriteLine("Feature\tPFI");
    
                foreach (var feature in featureImportanceMetrics)
                {
                    Console.WriteLine($"{featureColumnNames[feature.index],-20}|\t{feature.RSquared.Mean:F6}");
                }
            }
    
            private static void SaveModel(MLContext mlContext, ITransformer mlModel, string modelRelativePath, DataViewSchema modelInputSchema)
            {
                // Save/persist the trained model to a .ZIP file
                Console.WriteLine($"=============== Saving the model  ===============");
                mlContext.Model.Save(mlModel, modelInputSchema, GetAbsolutePath(modelRelativePath));
                Console.WriteLine("The model is saved to {0}", GetAbsolutePath(modelRelativePath));
            }
    
            public static string GetAbsolutePath(string relativePath)
            {
                FileInfo _dataRoot = new FileInfo(typeof(Program).Assembly.Location);
                string assemblyFolderPath = _dataRoot.Directory.FullName;
    
                string fullPath = Path.Combine(assemblyFolderPath, relativePath);
    
                return fullPath;
            }
    
            public static void PrintRegressionMetrics(RegressionMetrics metrics)
            {
                Console.WriteLine($"*************************************************");
                Console.WriteLine($"*       Metrics for Regression model      ");
                Console.WriteLine($"*------------------------------------------------");
                Console.WriteLine($"*       LossFn:        {metrics.LossFunction:0.##}");
                Console.WriteLine($"*       R2 Score:      {metrics.RSquared:0.##}");
                Console.WriteLine($"*       Absolute loss: {metrics.MeanAbsoluteError:#.##}");
                Console.WriteLine($"*       Squared loss:  {metrics.MeanSquaredError:#.##}");
                Console.WriteLine($"*       RMS loss:      {metrics.RootMeanSquaredError:#.##}");
                Console.WriteLine($"*************************************************");
            }
    
            public static void PrintRegressionFoldsAverageMetrics(IEnumerable<TrainCatalogBase.CrossValidationResult<RegressionMetrics>> crossValidationResults)
            {
                var L1 = crossValidationResults.Select(r => r.Metrics.MeanAbsoluteError);
                var L2 = crossValidationResults.Select(r => r.Metrics.MeanSquaredError);
                var RMS = crossValidationResults.Select(r => r.Metrics.RootMeanSquaredError);
                var lossFunction = crossValidationResults.Select(r => r.Metrics.LossFunction);
                var R2 = crossValidationResults.Select(r => r.Metrics.RSquared);
    
                Console.WriteLine($"*************************************************************************************************************");
                Console.WriteLine($"*       Metrics for Regression model      ");
                Console.WriteLine($"*------------------------------------------------------------------------------------------------------------");
                Console.WriteLine($"*       Average L1 Loss:       {L1.Average():0.###} ");
                Console.WriteLine($"*       Average L2 Loss:       {L2.Average():0.###}  ");
                Console.WriteLine($"*       Average RMS:           {RMS.Average():0.###}  ");
                Console.WriteLine($"*       Average Loss Function: {lossFunction.Average():0.###}  ");
                Console.WriteLine($"*       Average R-squared:     {R2.Average():0.###}  ");
                Console.WriteLine($"*************************************************************************************************************");
            }
        }
    }
    
  5. JakeRadMSFT commented on Oct 27, 2020

    @JakeRadMSFT
    Member

    @chunky did the example code help?

    We're planning to make these easier in upcoming versions of ML.NET and Tooling

  6. JakeRadMSFT commented on Dec 7, 2020

    @JakeRadMSFT
    Member

    @chunky closing for now ... hopefully the example code helped. We'll work on making the API easier to use in the future.

  7. chunky commented on Dec 21, 2020

    @chunky
    Author

    The example code helped a lot, thanks!

    I would recommend that something be created automatically by the wizard that demonstrates it for the specific model chosen by that wizard, but the sample got me sorted for now, thank-you

  8. changed the title [-]Can't get started with Explainability after ModelBuilder[/-] [+]Add Explainability to ModelBuilder[/+] on Feb 12, 2021
  9. luisquintanilla commented on Sep 22, 2022

    @luisquintanilla
    Contributor

    Not sure why this issue was reopened. Closing

  10. sfiruch commented on Nov 12, 2022

    @sfiruch

    @luisquintanilla This is still very much a real problem. I just wasted hours, having exactly the same problem as OP... Maybe you should re-consider closing this? Ideally, this would be obvious or work out of the box. Having to stumble across demo code in this GH issue while trying to report the same thing is neither.

  11. luisquintanilla commented on Nov 14, 2022

    @luisquintanilla
    Contributor

    Thanks for the discussion everyone. In the next few months we plan on making improvements to our generated code. This would be a great addition. Reopening it and tracking it as part of the #1638 issue.

  12. JakeRadMSFT commented on Nov 29, 2022

    @JakeRadMSFT
    Member

    I think ML.NET Repo has a comment on when PFI works and when it would take forever.

  13. added this to the January 2023 milestone on Jan 12, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions