classdef ClassificationLinear < ...
        classreg.learning.classif.ClassificationModel & classreg.learning.Linear
%ClassificationLinear Linear model for classification.
%   ClassificationLinear is a linear model for classification with one or
%   two classes. This model can predict responses for new data.
%
%   ClassificationLinear properties:
%       PredictorNames         - Names of predictors used for this model.
%       ExpandedPredictorNames - Names of expanded predictors.
%       ResponseName           - Name of the response variable.
%       ClassNames             - Names of classes in Y.
%       Cost                   - Misclassification costs.
%       Prior                  - Prior class probabilities.
%       ScoreTransform         - Transformation applied to predicted classification scores.
%       Learner                - Learned model, 'svm' or 'logistic'.
%       Beta                   - Linear coefficients.
%       Bias                   - Bias term.
%       FittedLoss             - Fitted loss function, 'hinge' or 'logit'.
%       Lambda                 - Regularization strength.
%       Regularization         - Type of regularization, L1 or L2.
%
%   ClassificationLinear methods:
%       edge                  - Classification edge.
%       incrementalLearner    - Return an incremental binary classification linear model.
%       loss                  - Classification loss.
%       margin                - Classification margins.
%       partialDependence     - Partial Dependence values.
%       plotPartialDependence - Plot Partial Dependence.
%       predict               - Predict responses of this model.
%       selectModels          - Select a subset of fitted regularized models.
%       update                - Update model parameters for code generation.

%   Copyright 2015-2023 The MathWorks, Inc.

    methods(Hidden=true)
        function this = ClassificationLinear(...
                dataSummary,classSummary,scoreTransform)
            % Protect against being called by the user
            if ~isstruct(dataSummary)
                error(message('stats:ClassificationLinear:ClassificationLinear:DoNotUseConstructor'));
            end

            this = this@classreg.learning.classif.ClassificationModel(...
                dataSummary,classSummary,scoreTransform,[]);
            this = this@classreg.learning.Linear;
            this.CategoricalVariableCoding = 'dummy';
        end

        function cmp = compact(this)
            cmp = this;
        end

        function compareHoldout(~,varargin)
            error(message('stats:ClassificationLinear:compareHoldout:DoNotUseCompareHoldout'));
        end
    end


    methods(Access=protected)
        function cl = getContinuousLoss(this)
            cl = [];
            lossfun = this.Impl.LossFunction;
            switch lossfun
                case 'logit'
                    if     isequal(this.PrivScoreTransform,@classreg.learning.transform.identity)
                        cl = @classreg.learning.loss.logit;
                    elseif isequal(this.PrivScoreTransform,@classreg.learning.transform.logit)
                        cl = @classreg.learning.loss.quadratic;
                    end

                case 'hinge'
                    if isequal(this.PrivScoreTransform,@classreg.learning.transform.identity)
                        cl = @classreg.learning.loss.hinge;
                    end
            end
        end

        function s = propsForDisp(this,s)
            s = propsForDisp@classreg.learning.classif.ClassificationModel(this,s);
            s = propsForDisp@classreg.learning.Linear(this,s);
            s = rmfield(s,'CategoricalPredictors');
        end

        function S = score(this,X,obsInRows)
            if any(this.CategoricalPredictors)
                if ~this.TableInput
                    % Explicit treatment of numerical predictors as categorical
                    % (for tables index encoding is done in table2FitMatrix
                    % or table2PredictMatrix)
                    X = classreg.learning.internal.encodeCategorical(X,this.VariableRange,obsInRows);
                end
                X = classreg.learning.internal.expandCategorical(X,...
                    this.CategoricalPredictors,this.OrdinalPredictors,...
                    this.VariableRange,this.ReferenceLevels,obsInRows);
            end
            S = score(this.Impl,X,true,obsInRows);
        end

        function n = getExpandedPredictorNames(this)
            n = classreg.learning.internal.expandPredictorNamesWithReference(...
                   this.PredictorNames,this.CategoricalPredictors,...
                   this.OrdinalPredictors,this.VariableRange,...
                   this.ReferenceLevels);
        end

        function this = gatherOne(this)
            % Gathers a single ClassificationLinear object. Used by
            % ClassificationLinear/gather
            this = gatherOne@classreg.learning.Linear(this);
            [...
                this.ClassSummary.ClassNames, ...
                this.ClassSummary.NonzeroProbClasses, ...
                this.ClassSummary.Prior, ...
                this.DataSummary.RowsUsed] = gather(...
                this.ClassSummary.ClassNames, ...
                this.ClassSummary.NonzeroProbClasses, ...
                this.ClassSummary.Prior, ...
                this.DataSummary.RowsUsed);
        end
    end


    methods
        function [labels,scores] = predict(this,X,varargin)
        %PREDICT Predict responses of the model.
        %   [LABEL,SCORE]=PREDICT(MODEL,X) returns predicted class labels and
        %   scores for linear model MODEL and predictors X. X must be a numeric
        %   matrix if MODEL was originally trained on a matrix and a table if MODEL
        %   was originally trained on a table. If X is a matrix, it must have size
        %   N-by-P, where P is the number of predictors used for training. If X is
        %   a table, it must contain all the predictors used for training the model.
        %   LABEL is an N-by-L array of classification labels with the same data
        %   type as the response variable used for training, where N is the number
        %   of observations (rows in X) and L is the number of values of the
        %   regularization parameter saved in the Lambda property of MODEL. SCORE
        %   is an N-by-K-by-L numeric matrix of scores for N observations, K
        %   classes, and L values of Lambda. The predicted label is assigned to the
        %   class with the largest score.
        %
        %   [LABEL,SCORE]=(MODEL,X,'PARAM1',val1,'PARAM2',val2,...) specifies
        %   optional parameter name/value pairs:
        %       'ObservationsIn'    - String specifying the data orientation,
        %                             either 'rows' or 'columns'. Default: 'rows'
        %                           NOTE: Passing observations in columns can
        %                                 significantly speed up prediction.
        %
        %   See also ClassificationLinear, Lambda.

            [varargin{:}] = convertStringsToChars(varargin{:});

            % Handle input data such as "tall" requiring a special adapter
            adapter = classreg.learning.internal.makeClassificationModelAdapter(this,X,varargin{:});
            if ~isempty(adapter)
                [labels,scores] = predict(adapter,X,varargin{:});
                return
            end

            % Convert to matrix X if necessary
            if this.TableInput || istable(X)
                X = classreg.learning.internal.table2PredictMatrix(X,[],[],...
                    this.VariableRange,this.CategoricalPredictors,this.PredictorNames);
            end

            internal.stats.checkSupportedNumeric('X',X,false,true,false,true);

            % Detect the orientation (do not transpose as Linear models
            % know how to handle both orientations)
            [X,obsInRows] = classreg.learning.internal.orientX(X,false,false,varargin{:});

            % Check num of predictors in data
            classreg.learning.internal.numPredictorsCheck(X,...
                getNumberPredictors(this),obsInRows)

            scorePrototype = internal.stats.dominantType(X, this.Beta, this.Bias);
            % Predictions for empty X
            if isempty(X)
                labels = repmat(this.ClassNames(1,:),0,1);
                K = numel(this.ClassSummary.ClassNames);
                scores = NaN(0,K,"like",scorePrototype);
                return;
            end

            % Predict
            S = score(this,X,obsInRows);

            [N,L] = size(S);

            % Match the class name order
            K = numel(this.ClassSummary.ClassNames);
            [~,pos] = ismember(this.ClassSummary.NonzeroProbClasses,...
                this.ClassSummary.ClassNames);

            % Match scores to class names.
            scores = NaN(N,K,L,'like',scorePrototype);
            for k=1:K
                scores(:,k,:) = -S;
            end
            if numel(pos)==1 % one-class learning
                scores(:,pos,:) = S;
            else             % binary learning
                scores(:,pos(2),:) = S;
            end

            prior = this.Prior;
            cost = this.Cost;
            scoreTransform = this.PrivScoreTransform;
            classnames = this.ClassNames;
            if ischar(classnames) && L>1
                classnames = cellstr(classnames);
            end
            labels = repmat(classnames(1,:),N,L);

            if L==1
                [labels,scores(:)] = ...
                    this.LabelPredictor(classnames,prior,cost,scores,scoreTransform);
            else
                for l=1:L
                    [labels(:,l),scores(:,:,l)] = ...
                        this.LabelPredictor(classnames,prior,cost,scores(:,:,l),scoreTransform);
                end
            end
        end

        function l = loss(this,X,varargin)
        %LOSS Classification error.
        %   ERR=LOSS(MODEL,X,Y) returns the classification error for model MODEL
        %   computed using predictors X and true class labels Y. X must be a
        %   numeric matrix if MODEL was originally trained on a matrix and a table
        %   if MODEL was originally trained on a table. If X is a matrix, it must
        %   have size N-by-P, where P is the number of predictors used for training.
        %   If X is a table, it must contain all the predictors used for training
        %   the model. Y must have the same data type as MODEL.ClassNames and have
        %   N elements. You can omit Y if X is a table that includes the response
        %   variable. ERR is a row vector with L values, where L is the number of
        %   values of the regularization parameter saved in the Lambda property of
        %   MODEL.
        %
        %   ERR=LOSS(MODEL,X,Y,'PARAM1',val1,'PARAM2',val2,...) specifies optional
        %   parameter name/value pairs:
        %       'ObservationsIn'   - String specifying the orientation of
        %                            X, either 'rows' or 'columns'.
        %                            Default: 'rows'
        %                          NOTE: Passing observations in columns can
        %                                significantly speed up prediction.
        %       'LossFun'          - Function handle for loss, or string
        %                            representing a built-in loss function.
        %                            Available loss functions for classification:
        %                            'binodeviance', 'classiferror', 'classifcost',
        %                            'exponential', 'hinge', 'logit', 'mincost',
        %                            and 'quadratic'. If you pass a function handle
        %                            FUN, LOSS calls it as shown below:
        %                                  FUN(C,S,W,COST)
        %                            where C is an N-by-K logical matrix for N
        %                            elements in Y and K classes in the ClassNames
        %                            property, S is an N-by-K numeric matrix, W is
        %                            a numeric vector with N elements, and COST is
        %                            a K-by-K numeric matrix. C has one true per
        %                            row for the true class. S is a matrix of
        %                            predicted scores for classes with one row per
        %                            observation, similar to SCORE output from
        %                            PREDICT. W is a vector of observation weights.
        %                            COST is a matrix of misclassification costs.
        %                            Default: 'classiferror'
        %       'Weights'          - Vector of observation weights. By default the
        %                            weight of every observation is set to 1. The
        %                            length of this vector must be equal to the
        %                            number of columns in X.
        %
        %   See also ClassificationLinear, predict, Lambda.
            l = loss@classreg.learning.classif.ClassificationModel(this,X,varargin{:});
        end

        function m = margin(this,X,varargin)
        %MARGIN Classification margins.
        %   M=MARGIN(MODEL,X,Y) returns classification margins obtained by MODEL
        %   for predictors X and class labels Y. X must be a numeric matrix if
        %   MODEL was originally trained on a matrix and a table if MODEL was
        %   originally trained on a table. If X is a matrix, it must have size
        %   N-by-P, where P is the number of predictors used for training. If X is
        %   a table, it must contain all the predictors used for training the
        %   model. Y must have the same data type as MODEL.ClassNames and have N
        %   elements. You can omit Y if X is a table that includes the response
        %   variable. The classification margin is the difference between the
        %   classification score for the true class and the maximal classification
        %   score for the false classes. The returned M is an N-by-L matrix for N
        %   observations and L values of the regularization parameter Lambda.
        %
        %   M=MARGIN(MODEL,X,Y,'PARAM1',val1,'PARAM2',val2,...) specifies optional
        %   parameter name/value pairs:
        %       'ObservationsIn'   - String specifying the orientation of
        %                            X, either 'rows' or 'columns'.
        %                            Default: 'rows'
        %                          NOTE: Passing observations in columns can
        %                                significantly speed up prediction.
        %
        %   See also ClassificationLinear, predict, Lambda.
            m = margin@classreg.learning.classif.ClassificationModel(this,X,varargin{:});
        end

        function e = edge(this,X,varargin)
        %EDGE Classification edge.
        %   E=EDGE(MODEL,X,Y) returns the classification edge obtained by MODEL for
        %   predictors X and class labels Y. X must be a numeric matrix if MODEL
        %   was originally trained on a matrix and a table if the MODEL was
        %   originally trained on a table. If X is a matrix, it must have size
        %   N-by-P, where P is the number of predictors used for training. If X is
        %   a table, it must contain all the predictors used for training the model.
        %   Y must have the same data type as MODEL.ClassNames and have N elements.
        %   You can omit Y if X is a table that includes the response variable. E
        %   is a row vector with L values, where L is the number of elements in
        %   Lambda. The classification edge is the classification margin averaged
        %   over the entire data.
        %
        %   E=EDGE(MODEL,X,Y,'PARAM1',val1,'PARAM2',val2,...) specifies optional
        %   parameter name/value pairs:
        %       'ObservationsIn'   - String specifying the orientation of
        %                            X, either 'rows' or 'columns'.
        %                            Default: 'rows'
        %                            NOTE: Passing observations in columns can
        %                                  significantly speed up prediction.
        %       'Weights'          - Observation weights, a numeric vector of
        %                            length size(X,1). By default, all observation
        %                            weights are set to 1. If you supply weights,
        %                            EDGE computes weighted classification edge.
        %
        %   See also ClassificationLinear, margin, Lambda.
            e = edge@classreg.learning.classif.ClassificationModel(this,X,varargin{:});
        end

        function newobj = update(obj,varargin)
        %UPDATE Update a ClassificationLinear model for code generation
        %   by using new parameters
        %
        %   UPDATEDMDL = UPDATE(MDL,PARAMS) returns an updated version of MDL
        %   that contains the new parameters in PARAMS.
        %
        %   Use the output of validatedUpdateInputs (<a href="matlab:doc classreg.learning.coder.config.svm.classificationsvmcoderconfigurer/validatedUpdateInputs">validatedUpdateInputs</a>) as the input
        %   PARAMS, a structure with a field for each parameter to
        %   update in the ClassificationLinear model.
        %
        %   The update parameters in PARAMS include Beta, Bias, Cost, and Prior.
        %
        %   See also learnerCoderConfigurer, validatedUpdateInputs,
        %      	classreg.learning.coder.config.linear.ClassificationLinearCoderConfigurer.


            % list of parameters that can be updated
            updateableParams = {'Beta','Bias','Prior','Cost'};

            upperbound = 2*numel(updateableParams)+1;
            narginchk(2,upperbound);

            try
                %validate inputs and return a parameter struct with new values
                paramStr = classreg.learning.coderutils.linear.validateLinearParamStruct(obj,true,updateableParams,varargin{:});

                % get struct representation of Impl object
                updatedModelStr = toStruct(obj);

                updatedModelStr.Impl.Beta = paramStr.Beta;
                updatedModelStr.Impl.Bias = paramStr.Bias;

                updatedModelStr = classreg.learning.coderutils.updatedClassificationStruct(updatedModelStr,paramStr);

                newobj = ClassificationLinear.fromStruct(updatedModelStr);
            catch ME
                throw(ME);
            end
        end

        function incrmdl = incrementalLearner(this,varargin)
        %INCREMENTALLEARNER Convert classification linear model to incremental learner.
        %   INCREMENTALMODEL=INCREMENTALLEARNER(MODEL) returns an incremental binary
        %   classification linear model INCREMENTALMODEL initialized
        %   using the parameters provided in MODEL.
        %
        %   INCREMENTALLEARNER uses these properties of MODEL to initialize
        %   INCREMENTALMODEL:
        %       'BatchSize'               - If MODEL.ModelParameters.Solver is 'sgd' or 'asgd',
        %                                   MODEL.BatchSize value is preserved.
        %                                   MODEL.BatchSize is ignored
        %                                   for scale-invariant solver.
        %       'Beta'
        %       'Bias'
        %       'ClassNames'              - Only binary classification is
        %                                   allowed.
        %       'FitBias'
        %       'Lambda'                  - If MODEL.Regularization is 'ridge (L2)':
        %                                       - If MODEL.Lambda is scalar, value is
        %                                         preserved.
        %                                       - If MODEL.Lambda is not scalar,
        %                                         use 'selectModels' prior to conversion
        %                                         in order to select a model with
        %                                         desired 'Lambda' value.
        %                                 - If MODEL.Regularization is 'lasso (L1)',
        %                                   MODEL.Lambda is ignored and 'Solver'
        %                                   is set to default 'scale-invariant' solver.
        %       'Learner'
        %       'LearnRate'               - If MODEL.ModelParameters.Solver is 'sgd' or 'asgd',
        %                                   MODEL.LearnRate value is preserved.
        %       'Prior'
        %       'Solver'                  - If MODEL.ModelParameters.Solver is 'sgd' or 'asgd',
        %                                   MODEL.ModelParameters.Solver value is preserved.
        %                                   Otherwise, default solver 'scale-invariant'
        %                                   is used.
        %
        %   INCREMENTALMODEL=INCREMENTALLEARNER(MODEL,'PARAM1',val1,'PARAM2',val2,...)
        %   specifies optional parameter name/value pairs:
        %       'EstimationPeriod'        - Nonnegative integer specifying the number of
        %                                   observations that are used for
        %                                   estimation of hyperparameters prior to
        %                                   training. For a positive integer, if
        %                                   model has unknown hyperparameters
        %                                   that require estimation,
        %                                   hyperparameters are estimated.
        %                                   Default value is 1000 when estimation is needed.
        %                                   Otherwise, default value is 0.
        %                                   Refer to documentation for more information.
        %       'Lambda'                  - Ridge (L2) regularization strength specified as a
        %                                   nonnegative scalar.
        %                                   If MODEL.ModelParameters.Solver is 'sgd' or
        %                                   'asgd' and MODEL.Regularization is
        %                                   'ridge (L2)', 'Lambda' is not supported.
        %                                   Otherwise, default value is 1e-5.
        %                                   'Lambda' is not supported for scale-invariant solver.
        %       'LearnRate'               - Learning rate for the stochastic
        %                                   solvers, specified either as 'auto' or as a positive
        %                                   scalar. If MODEL.ModelParameters.Solver is not 'sgd'
        %                                   and 'Solver' is specified
        %                                   as 'sgd', default is 'auto'.
        %                                   If MODEL.ModelParameters.Solver is not 'asgd'
        %                                   and 'Solver' is specified
        %                                   as 'asgd', default is 'auto'.
        %                                   'LearnRate' is not supported
        %                                   for scale-invariant solver.
        %                                   Refer to documentation for info
        %                                   on 'auto' behavior.
        %       'LearnRateSchedule'       - String, one of: 'decaying' or 'constant'.
        %                                   For stochastic solvers, if 'decaying',
        %                                   learning rate decreases over
        %                                   iterations. If 'constant', learning
        %                                   rate stays constant. If MODEL.ModelParameters.Solver is
        %                                   not 'sgd' or 'asgd'
        %                                   and 'Solver' is specified
        %                                   as 'sgd' or 'asgd', default is 'decaying'.
        %                                   'LearnRateSchedule' is not
        %                                   supported for scale-invariant solver.
        %       'Metrics'                 - String vector representing built-in
        %                                   metrics, cell array of function handles
        %                                   or a struct of function handles specifying
        %                                   metrics that are tracked during incremental
        %                                   training. 'Metrics' can be a
        %                                   combination of strings, function
        %                                   handles or struct of function handles
        %                                   passed as a cell array.
        %                                   Built-in metrics for classification:
        %                                   'binodeviance', 'classiferror', 'exponential',
        %                                   'hinge', 'logit', and 'quadratic'.
        %                                   If you pass a function handle FUN, it
        %                                   is called as shown below:
        %                                       FUN(C,S)
        %                                   where C is an N-by-K logical matrix for N
        %                                   elements in Y and K classes in the ClassNames
        %                                   property, S is an N-by-K numeric matrix.
        %                                   C has one true per row for the true class.
        %                                   S is a matrix of predicted scores for classes
        %                                   with one row per observation, similar to score
        %                                   output from predict.
        %                                   Metric name will be extracted from provided function
        %                                   handle if it's not an anonymous
        %                                   function handle. If it is an anonymous
        %                                   function handle, Metric will be named
        %                                   as 'CustomMetric' and it will be post-fixed
        %                                   with an underscore and an integer
        %                                   identifying its relative position in
        %                                   the Metrics list.
        %                                   If you pass a struct, fieldnames will
        %                                   be treated as the metric names and
        %                                   values must be function handles.
        %                                   Incremental models track two values for
        %                                   each metric; a 'Cumulative' value and a
        %                                   'Window' value. 'MetricsWindowSize' determines
        %                                   the update frequency of
        %                                   'Window' metrics.
        %                                   Default: 'classiferror'.
        %       'MetricsWarmupPeriod'     - Nonnegative integer specifying the number of
        %                                   observations that will be used for
        %                                   training the model prior to metrics
        %                                   update. Default is 0.
        %       'MetricsWindowSize'       - Positive integer specifying the window
        %                                   size that will be used for metrics
        %                                   update. Default value is 200.
        %       'Shuffle'                 - Logical value specifying
        %                                   whether observations
        %                                   are shuffled at each step.
        %                                   Default: true.
        %                                   'Shuffle' is not supported for
        %                                   stochastic solvers.
        %                                   Observations are always
        %                                   shuffled for stochastic
        %                                   solvers.
        %       'Standardize'             - Logical scalar. If true, standardize training data
        %                                   by centering and dividing columns by
        %                                   their standard deviations.
        %                                   Default is false.
        %       'Solver'                  - String, one of: 'sgd', 'asgd' or 'scale-invariant'.
        %                                   If 'sgd', stochastic gradient descent
        %                                   solver is used. If 'asgd', average
        %                                   stochastic gradient descent solver is
        %                                   used. If 'scale-invariant', adaptive
        %                                   scale-invariant solver is used.
        %                                   If MODEL.ModelParameters.Solver is 'sgd' or 'asgd',
        %                                   default is MODEL.ModelParameters.Solver.
        %                                   Otherwise, default is 'scale-invariant'.
        %
        %   Example: Initialize an incremental linear model using a traditionally trained model
        %   and continue incremental training.
        %
        %       ttModel = fitclinear(Xoffline,Yoffline);
        %       mdl = incrementalLearner(ttModel);
        %       mdl = updateMetricsAndFit(mdl,Xnew,Ynew,'Weights',Wnew);
        %
        %   See also incrementalClassificationLinear.
            assert(size(this.ClassNames,1) == 2,message('stats:incremental:Offline:OneClass'));
            [args,additionalargs] = incremental.utils.offlineLinearParameters(this,true,varargin{:});
            [args,additionalargs] = incremental.utils.offlineClassificationParameters(this,args,additionalargs);
            mergedargs = incremental.utils.mergeOfflineParameters(args,additionalargs);
            incrmdl = incrementalClassificationLinear(mergedargs);
        end
    end
    methods(Hidden)
        function s = toStruct(this,varargin)
            % Convert to a struct for codegen.

            warnState  = warning('query','all');
            warning('off','MATLAB:structOnObject');
            cleanupObj = onCleanup(@() warning(warnState));

            % convert common properties to struct
            s = classreg.learning.coderutils.classifToStruct(this);

            % Support only scalar lambda
            if ~isscalar(this.Lambda)
                error(message('stats:ClassificationLinear:toStruct:NonScalarLambda'));
            end

            % save the path to the fromStruct method
            s.FromStructFcn = 'ClassificationLinear.fromStruct';

            % learner char vector
            s.Learner = this.Learner;

            % model params
            s.ModelParams = classreg.learning.coderutils.linearParamsToCoderStruct(...
                this.ModelParams);

            % impl
            s.Impl = toStruct(this.Impl);

            s.DataSummary.NumExpandedPredictors = numel(getExpandedPredictorNames(this));
            s.DataSummary.NumCategories = cellfun(@numel,this.VariableRange);

            % if model has been trained with tall data, make necessary
            % modifications in compactStruct
            s = classreg.learning.coderutils.linear.handleModelTrainedWithTall(this,s);
        end
    end


    methods(Static,Hidden)
        function temp = template(varargin)
            classreg.learning.FitTemplate.catchType(varargin{:});
            temp = classreg.learning.FitTemplate.make('Linear',...
                'type','classification',varargin{:});
        end

        function [varargout] = fit(X,Y,varargin)
            temp = ClassificationLinear.template(varargin{:});
            [varargout{1:nargout}] = fit(temp,X,Y);
        end

        function obj = fromStruct(s)
            % Make a Linear object from a codegen struct.

            % check for 2016b compatibility
            if isfield(s,'ScoreTransformFull')
                s.ScoreTransform = s.ScoreTransformFull;
            end
            
            % Remove fields added for codegen
            if isfield(s.DataSummary,'NumExpandedPredictors')
                s.DataSummary = rmfield(s.DataSummary,'NumExpandedPredictors');
            end
            if isfield(s.DataSummary,'NumCategories')
                s.DataSummary = rmfield(s.DataSummary,'NumCategories');
            end

            % record modelParams for reconstruction
            modelParams = s.ModelParams;

            s = classreg.learning.coderutils.structToClassif(s);

            if isfield(s.DataSummary,'RemoveObservationsWithMissingValues')
                s.DataSummary = rmfield(s.DataSummary,'RemoveObservationsWithMissingValues');
            end
            
            % if model has been trained with tall data, make necessary
            % modifications in compactStruct           
            s = classreg.learning.coderutils.linear.handleModelTrainedWithTall(s);

            % Make an object
            obj = ClassificationLinear(s.DataSummary,s.ClassSummary,s.ScoreTransform);

            % Score handling
            if isempty(s.ScoreType)
                obj.ScoreType         = 'none';
            else
                obj.ScoreType         = s.ScoreType;
            end

            obj.DefaultLoss       = s.DefaultLoss;
            obj.LabelPredictor    = s.LabelPredictor;
            obj.DefaultScoreType  = s.DefaultScoreType;

            % Learner
            obj.Learner = s.Learner;

            % Model params
            obj.ModelParams = classreg.learning.coderutils.coderStructToLinearParams(modelParams);

            % restore Impl
            try
                obj.Impl = classreg.learning.impl.LinearImpl.fromStruct(s.Impl);
            catch ME
                if contains(ME.identifier,'stats:classreg:learning:impl:LinearImpl:make:BadStatus') % reset termination code MaxIter reached
                    s.Impl.FitInfo.TerminationCode = 0;
                    obj.Impl = classreg.learning.impl.LinearImpl.fromStruct(s.Impl);
                else
                    rethrow(ME);
                end
            end
        end


        function obj = makebeta(beta,bias,modelParams,dataSummary,classSummary,fitinfo,scoreTransform)
            % Makes a ClassificationLinear object without fitting by
            % feeding model parameters (beta and bias) and input model
            % parameters (modelParams)

            if isempty(scoreTransform)
                switch lower(modelParams.LossFunction)
                    case 'hinge'
                        scoreTransform = @classreg.learning.transform.identity;
                    case 'logit'
                        scoreTransform = @classreg.learning.transform.logit;
                end
            end

            obj = ClassificationLinear(dataSummary,classSummary,scoreTransform);

            obj.DefaultLoss = @classreg.learning.loss.classiferror;
            obj.LabelPredictor = @classreg.learning.classif.ClassificationModel.maxScore;
            obj.DefaultScoreType = 'inf';

            switch lower(modelParams.LossFunction)
                case 'hinge'
                    obj.Learner = 'svm';
                case 'logit'
                    obj.Learner = 'logistic';
                    if isequal(obj.PrivScoreTransform,@classreg.learning.transform.identity)
                        obj.ScoreTransform = 'logit';
                        obj.ScoreType = 'probability';
                    end
            end
            
            obj.Impl = classreg.learning.impl.LinearImpl.makeNoFit(modelParams,beta(:),bias,fitinfo);
            
            % Convert modelParams to a struct and remove Validation fields
            % to be consistent with fitClassificationLinear
            modelParams = toStruct(modelParams);
            modelParams = rmfield(modelParams,'ValidationX');
            modelParams = rmfield(modelParams,'ValidationY');
            modelParams = rmfield(modelParams,'ValidationW');
            obj.ModelParams = modelParams;
        end

        function [obj,fitInfo] = fitClassificationLinear(...
                X,Y,W,modelParams,dataSummary,classSummary,scoreTransform)
            obj = ClassificationLinear(dataSummary,classSummary,scoreTransform);

            [X,Y,W,dataSummary,classSummary] = ...
                ClassificationLinear.removeNaNsFromX(X,Y,W,dataSummary,classSummary);

            modelParams = fillIfNeeded(modelParams,X,Y,W,dataSummary,classSummary);

            if isa(X, "gpuArray") || isa(Y, "gpuArray") || isa(W, "gpuArray")
                iCheckGpuArrayLimitations(modelParams.Solver);
            end

            lossfun = modelParams.LossFunction;

            obj.DefaultLoss = @classreg.learning.loss.classiferror;
            obj.LabelPredictor = @classreg.learning.classif.ClassificationModel.maxScore;
            obj.DefaultScoreType = 'inf';

            switch lower(lossfun)
                case 'hinge'
                    obj.Learner = 'svm';

                case 'logit'
                    obj.Learner = 'logistic';
                    if isequal(obj.PrivScoreTransform,@classreg.learning.transform.identity)
                        obj.ScoreTransform = 'logit';
                        obj.ScoreType = 'probability';
                    end

            end

            % Map classes to -1 and +1
            gidx = grp2idx(Y,obj.ClassSummary.NonzeroProbClasses);
            if any(gidx==2)
                doclass = 2;
                gidx(gidx==1) = -1;
                gidx(gidx==2) = +1;
            else
                doclass = 1;
            end

            valgidx = [];
            if ~isempty(modelParams.ValidationY)
                valgidx = grp2idx(...
                    classreg.learning.internal.ClassLabel(modelParams.ValidationY),...
                    obj.ClassSummary.NonzeroProbClasses);
                if any(valgidx==2)
                    valgidx(valgidx==1) = -1;
                    valgidx(valgidx==2) =  1;
                end
            end

            lambda = modelParams.Lambda;
            if strcmp(lambda,'auto')
                lambda = 1/numel(gidx);
            end

            % Expand categorical predictors if necessary
            if any(obj.CategoricalPredictors)
                X = classreg.learning.internal.expandCategorical(...
                    X,obj.CategoricalPredictors,obj.OrdinalPredictors,...
                    obj.VariableRange,obj.ReferenceLevels,false);
                modelParams.ValidationX = classreg.learning.internal.expandCategorical(...
                    modelParams.ValidationX,obj.CategoricalPredictors,obj.OrdinalPredictors,...
                    obj.VariableRange,obj.ReferenceLevels,false);
            end

            if doclass==2
                W = classreg.learning.internal.adjustPrior(classSummary,Y,W);
            end

            obj.Impl = classreg.learning.impl.LinearImpl.make(doclass,...
                modelParams.InitialBeta,modelParams.InitialBias,...
                X,gidx,W,lossfun,...
                strcmp(modelParams.Regularization,'ridge'),...
                lambda,...
                modelParams.PassLimit,...
                modelParams.BatchLimit,...
                modelParams.NumCheckConvergence,...
                modelParams.BatchIndex,...
                modelParams.BatchSize,...
                modelParams.Solver,...
                modelParams.BetaTolerance,...
                modelParams.GradientTolerance,...
                modelParams.DeltaGradientTolerance,...
                modelParams.LearnRate,...
                modelParams.OptimizeLearnRate,...
                modelParams.ValidationX,valgidx,modelParams.ValidationW,...
                modelParams.IterationLimit,...
                modelParams.TruncationPeriod,...
                modelParams.FitBias,...
                modelParams.PostFitBias,...
                [],... % epsilon
                modelParams.HessianHistorySize,...
                modelParams.LineSearch,...
                0, ... % consensus strength
                modelParams.Stream,...
                modelParams.VerbosityLevel);

            modelParams = toStruct(modelParams);

            modelParams = rmfield(modelParams,'ValidationX');
            modelParams = rmfield(modelParams,'ValidationY');
            modelParams = rmfield(modelParams,'ValidationW');

            obj.ModelParams = modelParams;

            fitInfo                 = obj.Impl.FitInfo;
            fitInfo.Solver          = obj.Impl.Solver;
        end


        function [X,Y,W,dataSummary,classSummary] = ...
                removeNaNsFromX(X,Y,W,dataSummary,classSummary)
        % Remove observations (columns) that have at least one missing value

            nanX = any(isnan(X),1);
            if any(nanX)
                X(:,nanX)   = [];
                Y(nanX)     = [];
                W(nanX)     = [];
                rowsused = dataSummary.RowsUsed;
                if isempty(rowsused)
                    rowsused = ~nanX;
                else
                    rowsused(rowsused) = ~nanX;
                end
                dataSummary.RowsUsed = rowsused;
            end
            if all(nanX)
                error(message('stats:classreg:learning:FullClassificationRegressionModel:prepareDataCR:NoGoodXData'));
            end
            if any(nanX)
                warning(message('stats:classreg:learning:Linear:prepareDataCR:XwithNaNs'));
            end

            if any(nanX)
                % Renormalize weights
                W = W/sum(W);

                % Save initial values
                userClassNames = classSummary.ClassNames;
                nonzeroClassNames = classSummary.NonzeroProbClasses;
                prior = classSummary.Prior;
                cost = classSummary.Cost;

                % Get matrix of class weights
                C = classreg.learning.internal.classCount(nonzeroClassNames,Y);
                WC = C.*W;
                Wj = sum(WC,1);

                % Check prior
                prior = classreg.learning.classif.FullClassificationModel.processPrior(...
                    prior,Wj,userClassNames,nonzeroClassNames);

                % Get costs
                cost = classreg.learning.classif.FullClassificationModel.processCost(...
                    cost,prior,userClassNames,nonzeroClassNames);

                % Remove observations for classes with zero prior probabilities
                [X,Y,~,WC,Wj,prior,cost,nonzeroClassNames,dataSummary] = ...
                    ClassificationTree.removeZeroPriorAndCost(...
                    X,Y,C,WC,Wj,prior,cost,nonzeroClassNames,dataSummary);

                % Apply the average cost correction for 2 classes and set the
                % cost to the default value.
                if numel(nonzeroClassNames)>1
                    prior = prior.*sum(cost,2)';
                    cost = ones(2) - eye(2);
                end

                % Normalize priors in such a way that the priors in present
                % classes add up to one.  Normalize weights to add up to the
                % prior in the respective class.
                prior = prior/sum(prior);
                W = sum(WC.*(prior./Wj),2);

                % Put processed values into summary structure
                classSummary = ...
                    classreg.learning.classif.FullClassificationModel.makeClassSummary(...
                    userClassNames,nonzeroClassNames,prior,cost);
            end
        end


        function [X,Y,W,dataSummary,classSummary,scoreTransform] = ...
                prepareData(X,Y,varargin)
            [X,Y,vrange,wastable,varargin] = classreg.learning.internal.table2FitMatrix(X,Y,varargin{:});
            % Cast Y to the same storage type as X (for example, in memory
            % array or gpuArray). This ensures that the storage type of Y
            % does not affect how the model is fitted.
            Y = internal.stats.castToMatchingStorageTypeIfPossible(Y,X);

            % Process input args
            args = {'classnames' 'cost' 'prior' 'scoretransform' 'weights'};
            defs = {          []     []      []               [],       []};
            [userClassNames,cost,prior,transformer,W,~,crArgs] = ...
                internal.stats.parseArgs(args,defs,varargin{:});

            Y = classreg.learning.internal.ClassLabel(Y);

            % Get class names before any rows might be removed
            allClassNames = levels(Y);
            if isempty(allClassNames)
                error(message('stats:classreg:learning:classif:FullClassificationModel:prepareData:EmptyClassNames'));
            end

            % Check input type of X before using it for a numeric prototype
            if ~isfloat(X) || ~ismatrix(X)
                error(message('stats:classreg:learning:FullClassificationRegressionModel:prepareDataCR:BadXType'));
            end

            % Process Weights before arguments are passed to
            % Linear.prepareData to ensure it is dense, but otherwise like
            % X (same precision and storage type).
            weightsPrototype = full(internal.stats.numericPrototype(X));
            numObs = length(Y);
            if isempty(W)
                W = ones(numObs,1,"like",weightsPrototype);
            else
                % If weights is supplied ensure that the type of the
                % weights input does not influence how the model is fitted.
                W = cast(W, "like", weightsPrototype);
            end

            % Pre-process
            [X,Y,W,dataSummary] = classreg.learning.Linear.prepareDataCR(...
                X,Y,crArgs{:},...
                'Weights',W,'VariableRange',vrange,'TableInput',wastable);

            % Process class names
            [X,Y,W,userClassNames,nonzeroClassNames,dataSummary] = ...
                classreg.learning.classif.FullClassificationModel.processClassNames(...
                X,Y,W,userClassNames,allClassNames,dataSummary);

            internal.stats.checkSupportedNumeric('Weights',W,true,false,false,true);

            % Only binary and one-class learning is supported
            if numel(userClassNames)>2
                error(message('stats:ClassificationLinear:prepareData:DoNotPassMoreThanTwoClasses'));
            end

            % Sort nonzeroClassNames to make sure -1 corresponds to 1st
            % class and +1 corresponds to 2nd class passed by the user.
            [~,loc] = ismember(userClassNames,nonzeroClassNames);
            loc(loc==0) = [];
            nonzeroClassNames = nonzeroClassNames(loc);

            % Remove missing values
            if any(ismissing(Y))
                warning(message('stats:ClassificationLinear:prepareData:YwithMissingValues'));
            end
            [X,Y,W,dataSummary] = ...
                classreg.learning.classif.FullClassificationModel.removeMissingVals(X,Y,W,dataSummary);

            % Get matrix of class weights
            C = classreg.learning.internal.classCount(nonzeroClassNames,Y);
            WC = C.*W;
            Wj = sum(WC,1);

            % Check prior
            prior = classreg.learning.classif.FullClassificationModel.processPrior(...
                prior,Wj,userClassNames,nonzeroClassNames);

            % Get costs
            cost = classreg.learning.classif.FullClassificationModel.processCost(...
                cost,prior,userClassNames,nonzeroClassNames);

            % Remove observations for classes with zero prior probabilities
            [X,Y,~,WC,Wj,prior,cost,nonzeroClassNames,dataSummary] = ...
                ClassificationTree.removeZeroPriorAndCost(...
                X,Y,C,WC,Wj,prior,cost,nonzeroClassNames,dataSummary);

            % Normalize weights to add up to the prior in the respective
            % class.
            prior = prior/sum(prior);
            W = sum(WC.*(prior./Wj),2);

            % Put processed values into summary structure
            classSummary = ...
                classreg.learning.classif.FullClassificationModel.makeClassSummary(...
                userClassNames,nonzeroClassNames,prior,cost);

            % Make output score transformation
            scoreTransform = ...
                classreg.learning.classif.FullClassificationModel.processScoreTransform(transformer);
        end

        function name = matlabCodegenRedirect(~)
            name = 'classreg.learning.coder.classif.ClassificationLinear';
        end
    end
end

function iCheckGpuArrayLimitations(solver)
% Error if necessary for functionality not supported for GPU data.

% Only BFGS, LBFGS and SpaRSA solvers are supported
supportedSolvers = ["bfgs", "lbfgs", "sparsa"];
isSolverSupported = ismember(solver, supportedSolvers);
if ~all(isSolverSupported)
    unsupportedSolvers = solver(~isSolverSupported);
    throwAsCaller(MException(message("stats:ClassificationLinear:fitClassificationLinear:GpuArraySolverNotSupported", unsupportedSolvers{1})));
end
end

% LocalWords:  UPDATEDMDL classificationsvmcoderconfigurer Configurer INCREMENTALLEARNER
% LocalWords:  INCREMENTALMODEL Warmup tt Xoffline Yoffline Xnew Ynew Wnew Xwith Ywith
