From ec8985fe2f0d82aa8ac998e74536a699f4e1246f Mon Sep 17 00:00:00 2001 From: "REDMOND\\nakazmi" Date: Wed, 10 Jul 2019 15:38:47 -0700 Subject: [PATCH 1/2] Enable LinearSvmBinaryClassifier, add examples, add test, and update docs --- .../docstrings/LinearSvmBinaryClassifier.txt | 57 ++++++ src/python/nimbusml.pyproj | 6 + .../examples/LinearSvmBinaryClassifier.py | 37 ++++ .../LinearSvmBinaryClassifier_df.py | 31 +++ .../linear_model/linearsvmbinaryclassifier.py | 161 +++++++++++++++ src/python/nimbusml/linear_model/__init__.py | 2 + .../linear_model/linearsvmbinaryclassifier.py | 183 ++++++++++++++++++ .../test_linearsvmbinaryclassifier.py | 44 +++++ src/python/tools/manifest_diff.json | 8 + 9 files changed, 529 insertions(+) create mode 100644 src/python/docs/docstrings/LinearSvmBinaryClassifier.txt create mode 100644 src/python/nimbusml/examples/LinearSvmBinaryClassifier.py create mode 100644 src/python/nimbusml/examples/examples_from_dataframe/LinearSvmBinaryClassifier_df.py create mode 100644 src/python/nimbusml/internal/core/linear_model/linearsvmbinaryclassifier.py create mode 100644 src/python/nimbusml/linear_model/linearsvmbinaryclassifier.py create mode 100644 src/python/nimbusml/tests/linear_model/test_linearsvmbinaryclassifier.py diff --git a/src/python/docs/docstrings/LinearSvmBinaryClassifier.txt b/src/python/docs/docstrings/LinearSvmBinaryClassifier.txt new file mode 100644 index 00000000..df805518 --- /dev/null +++ b/src/python/docs/docstrings/LinearSvmBinaryClassifier.txt @@ -0,0 +1,57 @@ + """ + + Linear Support Vector Machine (SVM) Binary Classifier + + .. remarks:: + Linear SVM implements an algorithm that finds a hyperplane in the + feature space for binary classification, by solving an SVM problem. + For instance, with feature values *f_0, f_1,..., f_{D-1}*, the + prediction is given by determining what side of the hyperplane the + point falls into. That is the same as the sign of the feautures' + weighted sum, i.e. *\sum_{i = 0}^{D-1} \left(w_i * f_i \right) + b*, + where *w_0, w_1,..., w_{D-1}* are the weights computed by the + algorithm, and *b* is the bias computed by the algorithm. + + This algorithm implemented is the PEGASOS method, which alternates + between stochastic gradient descent steps and projection steps, + introduced by Shalev-Shwartz, Singer and Srebro. + + + **Reference** + + `Wikipedia entry for Support Vector Machine + `_ + + `Pegasos: Primal Estimated sub-GrAdient SOlver for SVM + `_ + + + :param normalize: Specifies the type of automatic normalization used: + + * ``"Auto"``: if normalization is needed, it is performed + automatically. This is the default choice. + * ``"No"``: no normalization is performed. + * ``"Yes"``: normalization is performed. + * ``"Warn"``: if normalization is needed, a warning + message is displayed, but normalization is not performed. + + Normalization rescales disparate data ranges to a standard scale. + Feature + scaling ensures the distances between data points are proportional + and + enables various optimization methods such as gradient descent to + converge + much faster. If normalization is performed, a ``MinMax`` normalizer + is + used. It normalizes values in an interval [a, b] where ``-1 <= a <= + 0`` + and ``0 <= b <= 1`` and ``b - a = 1``. This normalizer preserves + sparsity by mapping zero to zero. + + + .. index:: models, classification, svm + + Example: + .. literalinclude:: /../nimbusml/examples/LinearSvmBinaryClassifier.py + :language: python + """ diff --git a/src/python/nimbusml.pyproj b/src/python/nimbusml.pyproj index 1f344b47..ebf6b8c4 100644 --- a/src/python/nimbusml.pyproj +++ b/src/python/nimbusml.pyproj @@ -89,6 +89,7 @@ + @@ -140,6 +141,7 @@ + @@ -489,6 +491,7 @@ + @@ -531,6 +534,7 @@ + @@ -587,6 +591,7 @@ + @@ -860,6 +865,7 @@ + diff --git a/src/python/nimbusml/examples/LinearSvmBinaryClassifier.py b/src/python/nimbusml/examples/LinearSvmBinaryClassifier.py new file mode 100644 index 00000000..1a2d70e6 --- /dev/null +++ b/src/python/nimbusml/examples/LinearSvmBinaryClassifier.py @@ -0,0 +1,37 @@ +############################################################################### +# LinearSvmBinaryClassifier +from nimbusml import Pipeline, FileDataStream +from nimbusml.datasets import get_dataset +from nimbusml.linear_model import LinearSvmBinaryClassifier + +# data input (as a FileDataStream) +path = get_dataset('infert').as_filepath() + +data = FileDataStream.read_csv(path) +print(data.head()) +# age case education induced parity ... row_num spontaneous ... +# 0 26 1 0-5yrs 1 6 ... 1 2 ... +# 1 42 1 0-5yrs 1 1 ... 2 0 ... +# 2 39 1 0-5yrs 2 6 ... 3 0 ... +# 3 34 1 0-5yrs 2 4 ... 4 0 ... +# 4 35 1 6-11yrs 1 3 ... 5 1 ... +# define the training pipeline +pipeline = Pipeline([LinearSvmBinaryClassifier( + feature=['age', 'parity', 'spontaneous'], label='case')]) + +# train, predict, and evaluate +# TODO: Replace with CV +metrics, predictions = pipeline.fit(data).test(data, output_scores=True) + +# print predictions +print(predictions.head()) +# PredictedLabel Score Probability +# 0 1 0.688481 0.607060 +# 1 0 -2.514992 0.203312 +# 2 0 -3.479344 0.129230 +# 3 0 -3.016621 0.161422 +# 4 0 -0.825512 0.397461 +# print evaluation metrics +print(metrics) +# AUC Accuracy Positive precision Positive recall ... +# 0 0.705476 0.71371 0.666667 0.289157 ... diff --git a/src/python/nimbusml/examples/examples_from_dataframe/LinearSvmBinaryClassifier_df.py b/src/python/nimbusml/examples/examples_from_dataframe/LinearSvmBinaryClassifier_df.py new file mode 100644 index 00000000..a421dae8 --- /dev/null +++ b/src/python/nimbusml/examples/examples_from_dataframe/LinearSvmBinaryClassifier_df.py @@ -0,0 +1,31 @@ +############################################################################### +# AveragedPerceptronBinaryClassifier +import numpy as np +from nimbusml.datasets import get_dataset +from nimbusml.feature_extraction.categorical import OneHotVectorizer +from nimbusml.linear_model import LinearSvmBinaryClassifier +from sklearn.model_selection import train_test_split + +# use the built-in data set 'infert' to create test and train data +# Unnamed: 0 education age parity induced case spontaneous stratum \ +# 0 1 0.0 26.0 6.0 1.0 1.0 2.0 1.0 +# 1 2 0.0 42.0 1.0 1.0 1.0 0.0 2.0 +# pooled.stratum education_str +# 0 3.0 0-5yrs +# 1 1.0 0-5yrs +np.random.seed(0) + +df = get_dataset("infert").as_df() + +# remove : and ' ' from column names, and encode categorical column +df.columns = [i.replace(': ', '') for i in df.columns] +df = (OneHotVectorizer() << 'education_str').fit_transform(df) + +X_train, X_test, y_train, y_test = \ + train_test_split(df.loc[:, df.columns != 'case'], df['case']) + +lr = LinearSvmBinaryClassifier().fit(X_train, y_train) +scores = lr.predict(X_test) + +# Evaluate the model +print('Accuracy:', np.mean(y_test == [i for i in scores])) diff --git a/src/python/nimbusml/internal/core/linear_model/linearsvmbinaryclassifier.py b/src/python/nimbusml/internal/core/linear_model/linearsvmbinaryclassifier.py new file mode 100644 index 00000000..54bff625 --- /dev/null +++ b/src/python/nimbusml/internal/core/linear_model/linearsvmbinaryclassifier.py @@ -0,0 +1,161 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +# -------------------------------------------------------------------------------------------- +# - Generated by tools/entrypoint_compiler.py: do not edit by hand +""" +LinearSvmBinaryClassifier +""" + +__all__ = ["LinearSvmBinaryClassifier"] + + +from ...entrypoints.trainers_linearsvmbinaryclassifier import \ + trainers_linearsvmbinaryclassifier +from ...utils.utils import trace +from ..base_pipeline_item import BasePipelineItem, DefaultSignatureWithRoles + + +class LinearSvmBinaryClassifier( + BasePipelineItem, + DefaultSignatureWithRoles): + """ + + Linear Support Vector Machine (SVM) Binary Classifier + + .. remarks:: + Linear SVM implements an algorithm that finds a hyperplane in the + feature space for binary classification, by solving an SVM problem. + For instance, with feature values *f_0, f_1,..., f_{D-1}*, the + prediction is given by determining what side of the hyperplane the + point falls into. That is the same as the sign of the feautures' + weighted sum, i.e. *\sum_{i = 0}^{D-1} \left(w_i * f_i \right) + b*, + where *w_0, w_1,..., w_{D-1}* are the weights computed by the + algorithm, and *b* is the bias computed by the algorithm. + + This algorithm implemented is the PEGASOS method, which alternates + between stochastic gradient descent steps and projection steps, + introduced by Shalev-Shwartz, Singer and Srebro. + + + **Reference** + + `Wikipedia entry for Support Vector Machine + `_ + + `Pegasos: Primal Estimated sub-GrAdient SOlver for SVM + `_ + + + :param normalize: Specifies the type of automatic normalization used: + + * ``"Auto"``: if normalization is needed, it is performed + automatically. This is the default choice. + * ``"No"``: no normalization is performed. + * ``"Yes"``: normalization is performed. + * ``"Warn"``: if normalization is needed, a warning + message is displayed, but normalization is not performed. + + Normalization rescales disparate data ranges to a standard scale. + Feature + scaling ensures the distances between data points are proportional + and + enables various optimization methods such as gradient descent to + converge + much faster. If normalization is performed, a ``MinMax`` normalizer + is + used. It normalizes values in an interval [a, b] where ``-1 <= a <= + 0`` + and ``0 <= b <= 1`` and ``b - a = 1``. This normalizer preserves + sparsity by mapping zero to zero. + + :param caching: Whether trainer should cache input training data. + + :param lambda_: Regularizer constant. + + :param perform_projection: Perform projection to unit-ball? Typically used + with batch size > 1. + + :param number_of_iterations: Number of iterations. + + :param initial_weights_diameter: Sets the initial weights diameter that + specifies the range from which values are drawn for the initial + weights. These weights are initialized randomly from within this range. + For example, if the diameter is specified to be ``d``, then the weights + are uniformly distributed between ``-d/2`` and ``d/2``. The default + value is ``0``, which specifies that all the weights are set to zero. + + :param no_bias: No bias. + + :param initial_weights: Initial Weights and bias, comma-separated. + + :param shuffle: Whether to shuffle for each training iteration. + + :param batch_size: Batch size. + + :param params: Additional arguments sent to compute engine. + + .. index:: models, classification, svm + + Example: + .. literalinclude:: /../nimbusml/examples/LinearSvmBinaryClassifier.py + :language: python + """ + + @trace + def __init__( + self, + normalize='Auto', + caching='Auto', + lambda_=0.001, + perform_projection=False, + number_of_iterations=1, + initial_weights_diameter=0.0, + no_bias=False, + initial_weights=None, + shuffle=True, + batch_size=1, + **params): + BasePipelineItem.__init__( + self, type='classifier', **params) + + self.normalize = normalize + self.caching = caching + self.lambda_ = lambda_ + self.perform_projection = perform_projection + self.number_of_iterations = number_of_iterations + self.initial_weights_diameter = initial_weights_diameter + self.no_bias = no_bias + self.initial_weights = initial_weights + self.shuffle = shuffle + self.batch_size = batch_size + + @property + def _entrypoint(self): + return trainers_linearsvmbinaryclassifier + + @trace + def _get_node(self, **all_args): + algo_args = dict( + feature_column_name=self._getattr_role( + 'feature_column_name', + all_args), + label_column_name=self._getattr_role( + 'label_column_name', + all_args), + example_weight_column_name=self._getattr_role( + 'example_weight_column_name', + all_args), + normalize_features=self.normalize, + caching=self.caching, + lambda_=self.lambda_, + perform_projection=self.perform_projection, + number_of_iterations=self.number_of_iterations, + initial_weights_diameter=self.initial_weights_diameter, + no_bias=self.no_bias, + initial_weights=self.initial_weights, + shuffle=self.shuffle, + batch_size=self.batch_size) + + all_args.update(algo_args) + return self._entrypoint(**all_args) diff --git a/src/python/nimbusml/linear_model/__init__.py b/src/python/nimbusml/linear_model/__init__.py index 146c79e0..59c10b9e 100644 --- a/src/python/nimbusml/linear_model/__init__.py +++ b/src/python/nimbusml/linear_model/__init__.py @@ -3,6 +3,7 @@ from .fastlinearbinaryclassifier import FastLinearBinaryClassifier from .fastlinearclassifier import FastLinearClassifier from .fastlinearregressor import FastLinearRegressor +from .linearsvmbinaryclassifier import LinearSvmBinaryClassifier from .logisticregressionbinaryclassifier import \ LogisticRegressionBinaryClassifier from .logisticregressionclassifier import LogisticRegressionClassifier @@ -17,6 +18,7 @@ 'FastLinearBinaryClassifier', 'FastLinearClassifier', 'FastLinearRegressor', + 'LinearSvmBinaryClassifier', 'LogisticRegressionBinaryClassifier', 'LogisticRegressionClassifier', 'OnlineGradientDescentRegressor', diff --git a/src/python/nimbusml/linear_model/linearsvmbinaryclassifier.py b/src/python/nimbusml/linear_model/linearsvmbinaryclassifier.py new file mode 100644 index 00000000..30d1b927 --- /dev/null +++ b/src/python/nimbusml/linear_model/linearsvmbinaryclassifier.py @@ -0,0 +1,183 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +# -------------------------------------------------------------------------------------------- +# - Generated by tools/entrypoint_compiler.py: do not edit by hand +""" +LinearSvmBinaryClassifier +""" + +__all__ = ["LinearSvmBinaryClassifier"] + + +from sklearn.base import ClassifierMixin + +from ..base_predictor import BasePredictor +from ..internal.core.linear_model.linearsvmbinaryclassifier import \ + LinearSvmBinaryClassifier as core +from ..internal.utils.utils import trace + + +class LinearSvmBinaryClassifier( + core, + BasePredictor, + ClassifierMixin): + """ + + Linear Support Vector Machine (SVM) Binary Classifier + + .. remarks:: + Linear SVM implements an algorithm that finds a hyperplane in the + feature space for binary classification, by solving an SVM problem. + For instance, with feature values *f_0, f_1,..., f_{D-1}*, the + prediction is given by determining what side of the hyperplane the + point falls into. That is the same as the sign of the feautures' + weighted sum, i.e. *\sum_{i = 0}^{D-1} \left(w_i * f_i \right) + b*, + where *w_0, w_1,..., w_{D-1}* are the weights computed by the + algorithm, and *b* is the bias computed by the algorithm. + + This algorithm implemented is the PEGASOS method, which alternates + between stochastic gradient descent steps and projection steps, + introduced by Shalev-Shwartz, Singer and Srebro. + + + **Reference** + + `Wikipedia entry for Support Vector Machine + `_ + + `Pegasos: Primal Estimated sub-GrAdient SOlver for SVM + `_ + + + :param feature: see `Columns `_. + + :param label: see `Columns `_. + + :param weight: see `Columns `_. + + :param normalize: Specifies the type of automatic normalization used: + + * ``"Auto"``: if normalization is needed, it is performed + automatically. This is the default choice. + * ``"No"``: no normalization is performed. + * ``"Yes"``: normalization is performed. + * ``"Warn"``: if normalization is needed, a warning + message is displayed, but normalization is not performed. + + Normalization rescales disparate data ranges to a standard scale. + Feature + scaling ensures the distances between data points are proportional + and + enables various optimization methods such as gradient descent to + converge + much faster. If normalization is performed, a ``MinMax`` normalizer + is + used. It normalizes values in an interval [a, b] where ``-1 <= a <= + 0`` + and ``0 <= b <= 1`` and ``b - a = 1``. This normalizer preserves + sparsity by mapping zero to zero. + + :param caching: Whether trainer should cache input training data. + + :param lambda_: Regularizer constant. + + :param perform_projection: Perform projection to unit-ball? Typically used + with batch size > 1. + + :param number_of_iterations: Number of iterations. + + :param initial_weights_diameter: Sets the initial weights diameter that + specifies the range from which values are drawn for the initial + weights. These weights are initialized randomly from within this range. + For example, if the diameter is specified to be ``d``, then the weights + are uniformly distributed between ``-d/2`` and ``d/2``. The default + value is ``0``, which specifies that all the weights are set to zero. + + :param no_bias: No bias. + + :param initial_weights: Initial Weights and bias, comma-separated. + + :param shuffle: Whether to shuffle for each training iteration. + + :param batch_size: Batch size. + + :param params: Additional arguments sent to compute engine. + + .. index:: models, classification, svm + + Example: + .. literalinclude:: /../nimbusml/examples/LinearSvmBinaryClassifier.py + :language: python + """ + + @trace + def __init__( + self, + normalize='Auto', + caching='Auto', + lambda_=0.001, + perform_projection=False, + number_of_iterations=1, + initial_weights_diameter=0.0, + no_bias=False, + initial_weights=None, + shuffle=True, + batch_size=1, + feature=None, + label=None, + weight=None, + **params): + + if 'feature_column_name' in params: + raise NameError( + "'feature_column_name' must be renamed to 'feature'") + if feature: + params['feature_column_name'] = feature + if 'label_column_name' in params: + raise NameError( + "'label_column_name' must be renamed to 'label'") + if label: + params['label_column_name'] = label + if 'example_weight_column_name' in params: + raise NameError( + "'example_weight_column_name' must be renamed to 'weight'") + if weight: + params['example_weight_column_name'] = weight + BasePredictor.__init__(self, type='classifier', **params) + core.__init__( + self, + normalize=normalize, + caching=caching, + lambda_=lambda_, + perform_projection=perform_projection, + number_of_iterations=number_of_iterations, + initial_weights_diameter=initial_weights_diameter, + no_bias=no_bias, + initial_weights=initial_weights, + shuffle=shuffle, + batch_size=batch_size, + **params) + self.feature = feature + self.label = label + self.weight = weight + + @trace + def predict_proba(self, X, **params): + ''' + Returns probabilities + ''' + return self._predict_proba(X, **params) + + @trace + def decision_function(self, X, **params): + ''' + Returns score values + ''' + return self._decision_function(X, **params) + + def get_params(self, deep=False): + """ + Get the parameters for this operator. + """ + return core.get_params(self) diff --git a/src/python/nimbusml/tests/linear_model/test_linearsvmbinaryclassifier.py b/src/python/nimbusml/tests/linear_model/test_linearsvmbinaryclassifier.py new file mode 100644 index 00000000..19b2135f --- /dev/null +++ b/src/python/nimbusml/tests/linear_model/test_linearsvmbinaryclassifier.py @@ -0,0 +1,44 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +# -------------------------------------------------------------------------------------------- + +import unittest + +try: + # pandas 0.20.0+ + from pandas.api.types import is_string_dtype +except ImportError: + def is_string_dtype(dt): + return 'object' in str(dt) or "dtype('O')" in str(dt) + +import numpy as np +from nimbusml.feature_extraction.categorical import OneHotVectorizer +from nimbusml.linear_model import LinearSvmBinaryClassifier +from nimbusml.datasets import get_dataset +from nimbusml import Pipeline +from sklearn.model_selection import train_test_split +from sklearn.utils.testing import assert_greater + + +class TestLinearSvmBinaryClassifier(unittest.TestCase): + + def test_linearsvm(self): + np.random.seed(0) + df = get_dataset("infert").as_df() + # remove : and ' ' from column names, and encode categorical column + df.columns = [i.replace(': ', '') for i in df.columns] + assert is_string_dtype(df['education_str'].dtype) + df = (OneHotVectorizer() << ['education_str']).fit_transform(df) + assert 'education_str' not in df.columns + X_train, X_test, y_train, y_test = train_test_split( + df.loc[:, df.columns != 'case'], df['case'], random_state=0) + svm = LinearSvmBinaryClassifier(shuffle=False).fit(X_train, y_train) + scores = svm.predict(X_test) + accuracy = np.mean(y_test == [i for i in scores]) + assert_greater(accuracy, 0.96, "accuracy should be %s" % 0.96) + + +if __name__ == '__main__': + unittest.main() + diff --git a/src/python/tools/manifest_diff.json b/src/python/tools/manifest_diff.json index 58d6b3a5..0ff9aedf 100644 --- a/src/python/tools/manifest_diff.json +++ b/src/python/tools/manifest_diff.json @@ -235,6 +235,14 @@ "Predict_Proba" : true, "Decision_Function" : true }, + { + "Name": "Trainers.LinearSvmBinaryClassifier", + "NewName": "LinearSvmBinaryClassifier", + "Module": "linear_model", + "Type": "Classifier", + "Predict_Proba" : true, + "Decision_Function" : true + }, { "Name": "Transforms.ApproximateBootstrapSampler", "NewName": "BootstrapSampler", From 0d2ebaccc2fc83ba1114437edbfc9c5641f46cfb Mon Sep 17 00:00:00 2001 From: "REDMOND\\nakazmi" Date: Wed, 10 Jul 2019 17:39:47 -0700 Subject: [PATCH 2/2] Add test for predict_proba() and decision_function() --- .../test_linearsvmbinaryclassifier.py | 38 ++++++++++++++----- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/src/python/nimbusml/tests/linear_model/test_linearsvmbinaryclassifier.py b/src/python/nimbusml/tests/linear_model/test_linearsvmbinaryclassifier.py index 19b2135f..c144c377 100644 --- a/src/python/nimbusml/tests/linear_model/test_linearsvmbinaryclassifier.py +++ b/src/python/nimbusml/tests/linear_model/test_linearsvmbinaryclassifier.py @@ -18,12 +18,12 @@ def is_string_dtype(dt): from nimbusml.datasets import get_dataset from nimbusml import Pipeline from sklearn.model_selection import train_test_split -from sklearn.utils.testing import assert_greater +from sklearn.utils.testing import assert_almost_equal, assert_greater class TestLinearSvmBinaryClassifier(unittest.TestCase): - - def test_linearsvm(self): + @classmethod + def setUpClass(self): np.random.seed(0) df = get_dataset("infert").as_df() # remove : and ' ' from column names, and encode categorical column @@ -31,12 +31,32 @@ def test_linearsvm(self): assert is_string_dtype(df['education_str'].dtype) df = (OneHotVectorizer() << ['education_str']).fit_transform(df) assert 'education_str' not in df.columns - X_train, X_test, y_train, y_test = train_test_split( - df.loc[:, df.columns != 'case'], df['case'], random_state=0) - svm = LinearSvmBinaryClassifier(shuffle=False).fit(X_train, y_train) - scores = svm.predict(X_test) - accuracy = np.mean(y_test == [i for i in scores]) - assert_greater(accuracy, 0.96, "accuracy should be %s" % 0.96) + self.X_train, self.X_test, self.y_train, self.y_test = \ + train_test_split(df.loc[:, df.columns != 'case'], + df['case'], + random_state=0) + self.svm = LinearSvmBinaryClassifier(shuffle=False).fit(self.X_train, + self.y_train) + self.predictions = self.svm.predict(self.X_test) + self.accuracy = np.mean(self.y_test == [i for i in self.predictions]) + + def test_linearsvm(self): + assert_greater(self.accuracy, 0.96, "accuracy should be %s" % 0.96) + + def test_linearsvm_predict_proba(self): + probabilities = self.svm.predict_proba(self.X_test) + # Test that the class probabilities for each instance add up to 1 + [assert_almost_equal(probabilities[i][0] + probabilities[i][1], 1) \ + for i in range(probabilities.shape[0])] + + def test_linearsvm_decision_function(self): + fn = self.svm.decision_function(self.X_test) + predictions_from_fn = [fn[i] >= 0 for i in range(len(fn))] + assert [predictions_from_fn[i] == self.predictions[i] \ + for i in range(len(self.predictions))] + accuracy_from_fn = np.mean( + self.y_test == [i for i in predictions_from_fn]) + assert accuracy_from_fn == self.accuracy if __name__ == '__main__':