% GenericClassifier
%
% Abstract class providing kernel computation facilities
%   and evaluation helpers (should be really pulled to some super-class)
%
% This code is part of the supplementary material to the CVPR 2013 paper
% "From N to N+1: Multiclass Transfer Incremental Learning",
% Ilja Kuzborskij, Francesco Orabona and Barbara Caputo
%
% Copyright (C) 2012-2013, Ilja Kuzborskij
%
% This program is free software: you can redistribute it and/or modify
% it under the terms of the GNU General Public License as published by
% the Free Software Foundation, either version 3 of the License, or
% (at your option) any later version.
%
% This program is distributed in the hope that it will be useful,
% but WITHOUT ANY WARRANTY; without even the implied warranty of
% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
% GNU General Public License for more details.
%
% You should have received a copy of the GNU General Public License
% along with this program.  If not, see <http://www.gnu.org/licenses/>.
%
% Contact the author: ilja.kuzborskij [at] gmail.com
%

classdef GenericClassifier < handle
    properties
        model = [];
        model_hyperparams = 0;
        parameters = 0;
    end

    methods (Abstract)
        train_model(hyperparameters, data_obj);
        predict(x);
    end

    methods

        function obj = GenericClassifier(parameters)
            obj.parameters = parameters;
        end

        function [encoded_labels, classes] = coding_ova(obj, labels, supp_labels)
        % Encodes label set 'labels' as One Vs All code matrix 'encoded_labels',
        % where each column corresponds to {-1/+1} code.
        % 'classes' is an array containing label numeric value at position where code is +1.
        %

        %
            classes = unique(labels);
            if nargin < 3
                supp_labels = []; end
            classes = sort([classes; supp_labels]);

            encoded_labels = zeros(numel(labels), numel(classes));

            i = 1;
            for l=classes'
                tmp_y = labels;
                l_idx = tmp_y==l;
                tmp_y(l_idx) = 1;
                tmp_y(~l_idx) = -1;

                encoded_labels(:, i) = tmp_y;
                i = i + 1;
            end
        end

        function [Kmean, hyperparameters] = compute_mean_kernel(obj, hyperparameters, samples, samples2)
        % Computes averaged kernel provided hyperparameters and two sample sets.
        % Hyperparameter structure format is compatible with DOGMA compute_kernel format.
        %
            if isfield(hyperparameters, 'svm_best_gamma')
                hyperparameters.gamma = hyperparameters.svm_best_gamma; % compatibility
            end

            hyperparameters.type = hyperparameters.kernel_type;

            if nargin == 3
                samples2 = samples;
            end

            for r=1:numel(samples)
                Ktr(:,:,r)=compute_kernel(samples{r}', samples2{r}', hyperparameters);
            end

            Kmean=mean(Ktr,3);
        end

        function [Kmean, hyperparameters] = compute_mean_kernel_combinations(obj, hyperparameters, samples, samples2)
            if nargin == 3
                samples2 = samples;
            end
            [Kmean, hyperparameters] = GenericClassifier.compute_mean_kernel_combinations_eff_(hyperparameters, samples, samples2);
        end

        function train(obj, hyperparameters, x, y)
        % Convenience function
            data_obj.train_x = x;
            data_obj.train_y = y;
            obj.train_model(hyperparameters, data_obj);
        end

        function rs = evaluate(obj, x, y)
            data_obj.test_x = x;
            data_obj.test_y = y;
            rs = obj.evaluate_model(data_obj);
        end

        function result_obj = evaluate_model(obj, data_obj)
        % Computes basic evaluation criteria by using predict method (implemented in subclass).

            result_obj = [];
            if ~isempty(obj.model)

                % Accuracy
                [pred, pred_margins] = obj.predict(data_obj.test_x);
                accuracy = sum(pred==data_obj.test_y)/numel(pred);
                result_obj.test_accuracy = accuracy;

                class_map = obj.model.class_map;
                if isfield(obj.model, 'predict_class_map')
                    class_map = obj.model.predict_class_map;
                end

                % Accuracy separation for N and +1
                if numel(data_obj.test_y) > 1 && isfield(obj.parameters, 'new_class_label')
                    % Ordering always implies that new class is the last one in conf. matrix
                    conf_mat = confusionmat(data_obj.test_y, pred, 'order', ...
                                            [class_map(class_map~=obj.parameters.new_class_label); ...
                                             obj.parameters.new_class_label]);

                    cf_diag = diag(conf_mat);
                    cf_total_samp = sum(conf_mat, 2);

                    result_obj.n_accuracy = mean(cf_diag(1:end-1) ./ cf_total_samp(1:end-1));
                    result_obj.npp_accuracy = mean(cf_diag(end) ./ cf_total_samp(end));
                    result_obj.conf_mat = conf_mat;
                elseif numel(data_obj.test_y) > 1 && ~isfield(obj.parameters, 'new_class_label')
                    result_obj.n_accuracy = accuracy;
                    result_obj.npp_accuracy = 0;
                end

                % Accuracy separation for N and M
                if numel(data_obj.test_y) > 1 && isfield(obj.parameters, 'new_M_class_labels')
                    [conf_mat, order] = confusionmat___(data_obj.test_y, pred);
                    new_M_class_ix = ismember(order, obj.parameters.new_M_class_labels);

                    cf_diag = diag(conf_mat);
                    cf_total_samp = sum(conf_mat, 2);

                    result_obj.n_accuracy = mean(cf_diag(~new_M_class_ix) ./ cf_total_samp(~new_M_class_ix));
                    result_obj.m_accuracy = mean(cf_diag(new_M_class_ix) ./ cf_total_samp(new_M_class_ix));
                    result_obj.conf_mat = conf_mat;
                end

                % ROC w.r.t N and +1 & tp,fp,fn,tn
                if numel(data_obj.test_y) > 1 && isfield(obj.parameters, 'new_class_label')
                    margins = pred_margins(:, class_map == obj.parameters.new_class_label);
                    [junk, ix] = sort(margins, 'descend');

                    actual_y_ord = data_obj.test_y(ix);

                    pos_ix = actual_y_ord == obj.parameters.new_class_label;
                    actual_y_ord(pos_ix) = 1;
                    actual_y_ord(~pos_ix) = 0;

                    result_obj.roc.fpr = cumsum(actual_y_ord == 0) / sum(actual_y_ord == 0);
                    result_obj.roc.tpr = cumsum(actual_y_ord == 1) / sum(actual_y_ord == 1);

                    result_obj.new_TP = sum( (data_obj.test_y == obj.parameters.new_class_label) .* (pred == obj.parameters.new_class_label) );
                    result_obj.new_FP = sum( (data_obj.test_y ~= obj.parameters.new_class_label) .* (pred == obj.parameters.new_class_label) );
                    result_obj.new_FN = sum( (data_obj.test_y == obj.parameters.new_class_label) .* (pred ~= obj.parameters.new_class_label) );
                    result_obj.new_TN = sum( (data_obj.test_y ~= obj.parameters.new_class_label) .* (pred ~= obj.parameters.new_class_label) );
                end
            end
        end

        function clear(obj)
        % Clears parameter state
            obj.model = [];
        end

    end

    methods (Static)
        function [Kmean, hyperparameters] = compute_mean_kernel_combinations_(hyperparameters, samples, samples2)
        % Computes averaged kernel combinartions provided hyperparameters and two sample sets.
        % Hyperparameter structure format is compatible with OBSCURE compute_kernel format.
        % Combinations are computed as cartesian product of hyperparameter vectors, e.g.
        %  if gamma is a vector, will compute average of kernels with gammas as values in that vector.
        %
            hyperparameters.type = hyperparameters.kernel_type;

            i = 1;
            hypercomb = generate_parameter_combinations(hyperparameters);
            tens = zeros(size(samples{1}, 1), size(samples2{1}, 1), numel(hypercomb)*numel(samples));
            for hyper=hypercomb
                hyper = hyper{1};

                for r=1:numel(samples)
                    tens(:,:,i)=compute_kernel(samples{r}', samples2{r}', hyper);
                    i = i + 1;
                end
            end

            Kmean = mean(tens, 3);
        end

        function [Kmean, hyperparameters] = compute_mean_kernel_combinations_eff_(hyperparameters, samples, samples2)
        % Computes averaged kernel combinartions provided hyperparameters and two sample sets.
        % Hyperparameter structure format is compatible with OBSCURE compute_kernel format.
        % Combinations are computed as cartesian product of hyperparameter vectors, e.g.
        %  if gamma is a vector, will compute average of kernels with gammas as values in that vector.
        %
            hyperparameters.type = hyperparameters.kernel_type;

            Kmean = [];

            i = 0;
            hypercomb = generate_parameter_combinations(hyperparameters);
            for hyper=hypercomb
                hyper = hyper{1};

                for r=1:numel(samples)
                    if isempty(Kmean)
                        Kmean = compute_kernel(samples{r}', samples2{r}', hyper);
                    else
                        Kmean = Kmean + compute_kernel(samples{r}', samples2{r}', hyper);
                    end
                    i = i + 1;
                end
            end

            Kmean = Kmean ./ double(i);
        end
    end
end
