-
Baptiste Bauvin authoredBaptiste Bauvin authored
BoostUtils.py 30.99 KiB
import numpy as np
from sklearn.base import BaseEstimator, ClassifierMixin, TransformerMixin
from sklearn.preprocessing import LabelEncoder
from sklearn.utils.validation import check_is_fitted
import sys
import matplotlib.pyplot as plt
import datetime
class DecisionStumpClassifier(BaseEstimator, ClassifierMixin):
"""Generic Attribute Threshold Binary Classifier
Attributes
----------
attribute_index : int
The attribute to consider for the classification.
threshold : float
The threshold value for classification rule.
direction : int, optional
A multiplicative constant (1 or -1) to choose the "direction" of the stump. Defaults to 1. If -1, the stump
will predict the "negative" class (generally -1 or 0), and if 1, the stump will predict the second class (generally 1).
"""
def __init__(self, attribute_index, threshold, direction=1):
super(DecisionStumpClassifier, self).__init__()
self.attribute_index = attribute_index
self.threshold = threshold
self.direction = direction
def fit(self, X, y):
# Only verify that we are in the binary classification setting, with support for transductive learning.
if isinstance(y, np.ma.MaskedArray):
self.classes_ = np.unique(y[np.logical_not(y.mask)])
else:
self.classes_ = np.unique(y)
# This label encoder is there for the predict function to be able to return any two classes that were used
# when fitting, for example {-1, 1} or {0, 1}.
self.le_ = LabelEncoder()
self.le_.fit(self.classes_)
self.classes_ = self.le_.classes_
if not len(self.classes_) == 2:
raise ValueError('DecisionStumpsVoter only supports binary classification')
# assert len(self.classes_) == 2, "DecisionStumpsVoter only supports binary classification"
return self
def predict(self, X):
"""Returns the output of the classifier, on a sample X.
Parameters
----------
X : array-like, shape = [n_samples, n_features]
Training vectors, where n_samples is the number of samples and
n_features is the number of features.
Returns
-------
predictions : array-like, shape = [n_samples]
Predicted class labels.
"""
check_is_fitted(self, 'classes_')
import pdb;pdb.set_trace()
return self.le_.inverse_transform(np.argmax(self.predict_proba(X), axis=1))
def predict_proba(self, X):
"""Compute probabilities of possible outcomes for samples in X.
Parameters
----------
X : array-like, shape = [n_samples, n_features]
Training vectors, where n_samples is the number of samples and
n_features is the number of features.
Returns
-------
avg : array-like, shape = [n_samples, n_classes]
Weighted average probability for each class per sample.
"""
check_is_fitted(self, 'classes_')
X = np.asarray(X)
probas = np.zeros((X.shape[0], 2))
positive_class = np.argwhere(X[:, self.attribute_index] > self.threshold)
negative_class = np.setdiff1d(range(X.shape[0]), positive_class)
probas[positive_class, 1] = 1.0
probas[negative_class, 0] = 1.0
if self.direction == -1:
probas = 1 - probas
return probas
def predict_proba_t(self, X):
"""Compute probabilities of possible outcomes for samples in X.
Parameters
----------
X : array-like, shape = [n_samples, n_features]
Training vectors, where n_samples is the number of samples and
n_features is the number of features.
Returns
-------
avg : array-like, shape = [n_samples, n_classes]
Weighted average probability for each class per sample.
"""
try:
print('plouf')
print(X)
print("plaf")
except:
X=np.ones(X.shape)
check_is_fitted(self, 'classes_')
X = np.asarray(X)
probas = np.zeros((X.shape[0], 2))
positive_class = np.argwhere(X[:, self.attribute_index] > self.threshold)
negative_class = np.setdiff1d(range(X.shape[0]), positive_class)
probas[positive_class, 1] = 1.0
probas[negative_class, 0] = 1.0
if self.direction == -1:
probas = 1 - probas
return probas
def reverse_decision(self):
self.direction *= -1
class ClassifiersGenerator(BaseEstimator, TransformerMixin):
"""Base class to create a set of voters using training samples, and then transform a set of examples in
the voters' output space.
Attributes
----------
self_complemented : bool, optional
Whether or not a binary complement voter must be generated for each voter. Defaults to False.
voters : ndarray of voter functions
Once fit, contains the voter functions.
"""
def __init__(self, self_complemented=False):
super(ClassifiersGenerator, self).__init__()
self.self_complemented = self_complemented
def fit(self, X, y=None):
"""Generates the voters using training samples.
Parameters
----------
X : ndarray of shape (n_samples, n_features)
Input data on which to base the voters.
y : ndarray of shape (n_labeled_samples,), optional
Input labels, usually determines the decision polarity of each voter.
Returns
-------
self
"""
raise NotImplementedError
def transform(self, X):
"""Transforms the input points in a matrix of classification, using previously learned voters.
Parameters
----------
X : ndarray of shape (n_samples, n_features)
Input data to classify.
Returns
-------
ndarray of shape (n_samples, n_voters)
The voters' decision on each example.
"""
check_is_fitted(self, 'estimators_')
return np.array([voter.predict(X) for voter in self.estimators_]).T
# class TreesClassifiersGenerator(ClassifiersGenerator):
# """A generator to widen the voter's pool of our boosting algorithms.
# """
#
# def __init__(self, n_stumps_per_attribute=10, self_complemented=False, check_diff=True, max_depth=3):
# super(TreesClassifiersGenerator, self).__init__(self_complemented)
# self.n_stumps_per_attribute = n_stumps_per_attribute
# self.check_diff = check_diff
# self.max_depth = max_depth
#
# def fit(self, X, y=None):
class StumpsClassifiersGenerator(ClassifiersGenerator):
"""Decision Stump Voters transformer.
Parameters
----------
n_stumps_per_attribute : int, optional
Determines how many decision stumps will be created for each attribute. Defaults to 10.
No stumps will be created for attributes with only one possible value.
self_complemented : bool, optional
Whether or not a binary complement voter must be generated for each voter. Defaults to False.
"""
def __init__(self, n_stumps_per_attribute=10, self_complemented=False, check_diff=False):
super(StumpsClassifiersGenerator, self).__init__(self_complemented)
self.n_stumps_per_attribute = n_stumps_per_attribute
self.check_diff = check_diff
def fit(self, X, y=None):
"""Fits Decision Stump voters on a training set.
Parameters
----------
X : ndarray of shape (n_samples, n_features)
Input data on which to base the voters.
y : ndarray of shape (n_labeled_samples,), optional
Only used to ensure that we are in the binary classification setting.
Returns
-------
self
"""
minimums = np.min(X, axis=0)
maximums = np.max(X, axis=0)
if y.ndim > 1:
y = np.reshape(y, (y.shape[0], ))
ranges = (maximums - minimums) / (self.n_stumps_per_attribute + 1)
if self.check_diff:
nb_differents = [np.unique(col) for col in np.transpose(X)]
self.estimators_ = []
for i in range(X.shape[1]):
nb_different = nb_differents[i].shape[0]
different = nb_differents[i]
if nb_different-1 < self.n_stumps_per_attribute:
self.estimators_ += [DecisionStumpClassifier(i,
(different[stump_number]+different[
stump_number+1])/2, 1).fit(X, y)
for stump_number in range(int(nb_different)-1)]
if self.self_complemented:
self.estimators_ += [DecisionStumpClassifier(i,
(different[stump_number] + different[
stump_number + 1]) / 2, -1).fit(X, y)
for stump_number in range(int(nb_different)-1)]
else:
self.estimators_ += [DecisionStumpClassifier(i, minimums[i] + ranges[i] * stump_number, 1).fit(X, y)
for stump_number in range(1, self.n_stumps_per_attribute + 1)
if ranges[i] != 0]
if self.self_complemented:
self.estimators_ += [DecisionStumpClassifier(i, minimums[i] + ranges[i] * stump_number, -1).fit(X, y)
for stump_number in range(1, self.n_stumps_per_attribute + 1)
if ranges[i] != 0]
else:
self.estimators_ = [DecisionStumpClassifier(i, minimums[i] + ranges[i] * stump_number, 1).fit(X, y)
for i in range(X.shape[1]) for stump_number in range(1, self.n_stumps_per_attribute + 1)
if ranges[i] != 0]
if self.self_complemented:
self.estimators_ += [DecisionStumpClassifier(i, minimums[i] + ranges[i] * stump_number, -1).fit(X, y)
for i in range(X.shape[1]) for stump_number in range(1, self.n_stumps_per_attribute + 1)
if ranges[i] != 0]
self.estimators_ = np.asarray(self.estimators_)
return self
def _as_matrix(element):
""" Utility function to convert "anything" to a Numpy matrix.
"""
# If a scalar, return a 1x1 matrix.
if len(np.shape(element)) == 0:
return np.matrix([[element]], dtype=float)
# If a nd-array vector, return a column matrix.
elif len(np.shape(element)) == 1:
matrix = np.matrix(element, dtype=float)
if np.shape(matrix)[1] != 1:
matrix = matrix.T
return matrix
return np.matrix(element, dtype=float)
def _as_column_matrix(array_like):
""" Utility function to convert any array to a column Numpy matrix.
"""
matrix = _as_matrix(array_like)
if 1 not in np.shape(matrix):
raise ValueError("_as_column_vector: input must be a vector")
if np.shape(matrix)[0] == 1:
matrix = matrix.T
return matrix
def _as_line_matrix(array_like):
""" Utility function to convert any array to a line Numpy matrix.
"""
matrix = _as_matrix(array_like)
if 1 not in np.shape(matrix):
raise ValueError("_as_column_vector: input must be a vector")
if np.shape(matrix)[1] == 1:
matrix = matrix.T
return matrix
def sign(array):
"""Computes the elementwise sign of all elements of an array. The sign function returns -1 if x <=0 and 1 if x > 0.
Note that numpy's sign function can return 0, which is not desirable in most cases in Machine Learning algorithms.
Parameters
----------
array : array-like
Input values.
Returns
-------
ndarray
An array with the signs of input elements.
"""
signs = np.sign(array)
signs[array == 0] = -1
return signs
class ConvexProgram(object):
"""
Encapsulates a quadratic program of the following form:
minimize (1/2)*x'*P*x + q'*x
subject to G*x <= h
A*x = b.
or a linear program of the following form:
minimize c'*x
subject to G*x <= h
A*x = b
"""
def __init__(self):
self._quadratic_func = None
self._linear_func = None
self._inequality_constraints_matrix = None
self._inequality_constraints_values = None
self._equality_constraints_matrix = None
self._equality_constraints_values = None
self._lower_bound_values = None
self._upper_bound_values = None
self._n_variables = None
@property
def n_variables(self):
return self._n_variables
@property
def quadratic_func(self):
return self._quadratic_func
@quadratic_func.setter
def quadratic_func(self, quad_matrix):
quad_matrix = _as_matrix(quad_matrix)
n_lines, n_columns = np.shape(quad_matrix)
assert(n_lines == n_columns)
if self._linear_func is not None:
assert(np.shape(quad_matrix)[0] == self._n_variables)
else:
self._n_variables = n_lines
self._quadratic_func = quad_matrix
@property
def linear_func(self):
return self._linear_func
@linear_func.setter
def linear_func(self, lin_vector):
if lin_vector is not None:
lin_vector = _as_column_matrix(lin_vector)
if self._quadratic_func is not None:
assert(np.shape(lin_vector)[0] == self._n_variables)
else:
self._n_variables = np.shape(lin_vector)[0]
self._linear_func = lin_vector
def add_inequality_constraints(self, inequality_matrix, inequality_values):
if inequality_matrix is None:
return
self._assert_objective_function_is_set()
if 1 in np.shape(inequality_matrix) or len(np.shape(inequality_matrix)) == 1:
inequality_matrix = _as_line_matrix(inequality_matrix)
else:
inequality_matrix = _as_matrix(inequality_matrix)
inequality_values = _as_column_matrix(inequality_values)
assert np.shape(inequality_matrix)[1] == self._n_variables
assert np.shape(inequality_values)[1] == 1
if self._inequality_constraints_matrix is None:
self._inequality_constraints_matrix = inequality_matrix
else:
self._inequality_constraints_matrix = np.append(self._inequality_constraints_matrix,
inequality_matrix, axis=0)
if self._inequality_constraints_values is None:
self._inequality_constraints_values = inequality_values
else:
self._inequality_constraints_values = np.append(self._inequality_constraints_values,
inequality_values, axis=0)
def add_equality_constraints(self, equality_matrix, equality_values):
if equality_matrix is None:
return
self._assert_objective_function_is_set()
if 1 in np.shape(equality_matrix) or len(np.shape(equality_matrix)) == 1:
equality_matrix = _as_line_matrix(equality_matrix)
else:
equality_matrix = _as_matrix(equality_matrix)
equality_values = _as_matrix(equality_values)
assert np.shape(equality_matrix)[1] == self._n_variables
assert np.shape(equality_values)[1] == 1
if self._equality_constraints_matrix is None:
self._equality_constraints_matrix = equality_matrix
else:
self._equality_constraints_matrix = np.append(self._equality_constraints_matrix,
equality_matrix, axis=0)
if self._equality_constraints_values is None:
self._equality_constraints_values = equality_values
else:
self._equality_constraints_values = np.append(self._equality_constraints_values,
equality_values, axis=0)
def add_lower_bound(self, lower_bound):
if lower_bound is not None:
self._assert_objective_function_is_set()
self._lower_bound_values = np.array([lower_bound] * self._n_variables)
def add_upper_bound(self, upper_bound):
if upper_bound is not None:
self._assert_objective_function_is_set()
self._upper_bound_values = np.array([upper_bound] * self._n_variables)
def _convert_bounds_to_inequality_constraints(self):
self._assert_objective_function_is_set()
if self._lower_bound_values is not None:
c_matrix = []
for i in range(self._n_variables):
c_line = [0] * self._n_variables
c_line[i] = -1.0
c_matrix.append(c_line)
c_vector = _as_column_matrix(self._lower_bound_values)
self._lower_bound_values = None
self.add_inequality_constraints(np.matrix(c_matrix).T, c_vector)
if self._upper_bound_values is not None:
c_matrix = []
for i in range(self._n_variables):
c_line = [0] * self._n_variables
c_line[i] = 1.0
c_matrix.append(c_line)
c_vector = _as_column_matrix(self._upper_bound_values)
self._upper_bound_values = None
self.add_inequality_constraints(np.matrix(c_matrix).T, c_vector)
def _convert_to_cvxopt_matrices(self):
from cvxopt import matrix as cvxopt_matrix
if self._quadratic_func is not None:
self._quadratic_func = cvxopt_matrix(self._quadratic_func)
if self._linear_func is not None:
self._linear_func = cvxopt_matrix(self._linear_func)
else:
# CVXOPT needs this vector to be set even if it is not used, so we put zeros in it!
self._linear_func = cvxopt_matrix(np.zeros((self._n_variables, 1)))
if self._inequality_constraints_matrix is not None:
self._inequality_constraints_matrix = cvxopt_matrix(self._inequality_constraints_matrix)
if self._inequality_constraints_values is not None:
self._inequality_constraints_values = cvxopt_matrix(self._inequality_constraints_values)
if self._equality_constraints_matrix is not None:
self._equality_constraints_matrix = cvxopt_matrix(self._equality_constraints_matrix)
if self._equality_constraints_values is not None:
self._equality_constraints_values = cvxopt_matrix(self._equality_constraints_values)
def _assert_objective_function_is_set(self):
assert self._n_variables is not None
def solve(self, solver="cvxopt", feastol=1e-7, abstol=1e-7, reltol=1e-6, return_all_information=False):
# Some solvers are very verbose, and we don't want them to pollute STDOUT or STDERR.
original_stdout = sys.stdout
original_stderr = sys.stderr
ret = None
# TODO: Repair
# if solver == "cvxopt":
# stdout_logger = logging.getLogger('CVXOPT')
# sl = StreamToLogger(stdout_logger, logging.DEBUG)
# sys.stdout = sl
# stderr_logger = logging.getLogger('CVXOPT')
# sl = StreamToLogger(stderr_logger, logging.WARNING)
# sys.stderr = sl
try:
if solver == "cvxopt":
from cvxopt.solvers import qp, lp, options
options['feastol'] = feastol
options['abstol'] = abstol
options['reltol'] = reltol
options['show_progress'] = False
self._convert_bounds_to_inequality_constraints()
self._convert_to_cvxopt_matrices()
if self._quadratic_func is not None:
ret = qp(self.quadratic_func, self.linear_func, self._inequality_constraints_matrix,
self._inequality_constraints_values, self._equality_constraints_matrix,
self._equality_constraints_values)
else:
ret = lp(self.linear_func,
G=self._inequality_constraints_matrix,
h=self._inequality_constraints_values,
A=self._equality_constraints_matrix,
b=self._equality_constraints_values)
#logging.info("Primal objective value = {}".format(ret['primal objective']))
#logging.info("Dual objective value = {}".format(ret['dual objective']))
if not return_all_information:
ret = np.asarray(np.array(ret['x']).T[0])
elif solver == "cplex":
import cplex
p = cplex.Cplex()
p.objective.set_sense(p.objective.sense.minimize)
# This is ugly. CPLEX wants a list of lists of lists. First dimension represents the lines of the QP
# matrix. Second dimension contains a pair of two elements: the indices of the variables in play (all of
# them...), and the values (columns of the QP matrix).
names = [str(x) for x in range(self._n_variables)]
p.variables.add(names=names)
if self.quadratic_func is not None:
p_matrix = []
for line in self._quadratic_func:
p_matrix.append([names, line.tolist()[0]])
p.objective.set_quadratic(p_matrix)
if self.linear_func is not None:
p.objective.set_linear(zip(names,
np.asarray(self.linear_func.T).reshape(self.n_variables,).tolist()))
if self._inequality_constraints_matrix is not None:
inequality_linear = []
for line in self._inequality_constraints_matrix:
inequality_linear.append([names, line.tolist()[0]])
p.linear_constraints.add(lin_expr=inequality_linear,
rhs=np.asarray(self._inequality_constraints_values.T).tolist()[0],
senses="L"*len(self._inequality_constraints_values))
if self._equality_constraints_matrix is not None:
equality_linear = []
for line in self._equality_constraints_matrix:
equality_linear.append([names, line.tolist()[0]])
p.linear_constraints.add(lin_expr=equality_linear,
rhs=np.asarray(self._equality_constraints_values.T).tolist()[0],
senses="E"*len(self._equality_constraints_values))
if self._lower_bound_values is not None:
p.variables.set_lower_bounds(zip(names, self._lower_bound_values))
if self._upper_bound_values is not None:
p.variables.set_upper_bounds(zip(names, self._upper_bound_values))
p.solve()
if not return_all_information:
ret = np.array(p.solution.get_values())
else:
ret = {'primal': np.array(p.solution.get_values()),
'dual': np.array(p.solution.get_dual_values())}
elif solver == "pycpx":
# This shows how easy it is to use pycpx. However, it is much slower (as it is more versatile!).
import pycpx
model = pycpx.CPlexModel(verbosity=2)
q = model.new(self.n_variables)
if self._inequality_constraints_matrix is not None:
model.constrain(self._inequality_constraints_matrix * q <= self._inequality_constraints_values)
if self._equality_constraints_matrix is not None:
model.constrain(self._equality_constraints_matrix * q == self._equality_constraints_values)
if self._lower_bound_values is not None:
model.constrain(q >= self._lower_bound_values)
if self._upper_bound_values is not None:
model.constrain(q <= self._upper_bound_values)
value = model.minimize(0.5 * q.T * self._quadratic_func * q + self.linear_func.T * q)
if not return_all_information:
ret = np.array(model[q])
else:
ret = model
except:
raise
finally:
sys.stdout = original_stdout
sys.stderr = original_stderr
return ret
def _as_matrix(element):
""" Utility function to convert "anything" to a Numpy matrix.
"""
# If a scalar, return a 1x1 matrix.
if len(np.shape(element)) == 0:
return np.matrix([[element]], dtype=float)
# If a nd-array vector, return a column matrix.
elif len(np.shape(element)) == 1:
matrix = np.matrix(element, dtype=float)
if np.shape(matrix)[1] != 1:
matrix = matrix.T
return matrix
return np.matrix(element, dtype=float)
def _as_column_matrix(array_like):
""" Utility function to convert any array to a column Numpy matrix.
"""
matrix = _as_matrix(array_like)
if 1 not in np.shape(matrix):
raise ValueError("_as_column_vector: input must be a vector")
if np.shape(matrix)[0] == 1:
matrix = matrix.T
return matrix
def _as_line_matrix(array_like):
""" Utility function to convert any array to a line Numpy matrix.
"""
matrix = _as_matrix(array_like)
if 1 not in np.shape(matrix):
raise ValueError("_as_column_vector: input must be a vector")
if np.shape(matrix)[1] == 1:
matrix = matrix.T
return matrix
def sign(array):
"""Computes the elementwise sign of all elements of an array. The sign function returns -1 if x <=0 and 1 if x > 0.
Note that numpy's sign function can return 0, which is not desirable in most cases in Machine Learning algorithms.
Parameters
----------
array : array-like
Input values.
Returns
-------
ndarray
An array with the signs of input elements.
"""
signs = np.sign(array)
signs[array == 0] = -1
return signs
def get_accuracy_graph(plotted_data, classifier_name, file_name, name="Accuracies", bounds=None, bound_name=None, boosting_bound=None, set="train", zero_to_one=True):
if type(name) is not str:
name = " ".join(name.getConfig().strip().split(" ")[:2])
f, ax = plt.subplots(nrows=1, ncols=1)
if zero_to_one:
ax.set_ylim(bottom=0.0,top=1.0)
ax.set_title(name+" during "+set+" for "+classifier_name)
x = np.arange(len(plotted_data))
scat = ax.scatter(x, np.array(plotted_data), marker=".")
if bounds:
if boosting_bound:
scat2 = ax.scatter(x, boosting_bound, marker=".")
scat3 = ax.scatter(x, np.array(bounds), marker=".", )
ax.legend((scat, scat2, scat3), (name,"Boosting bound", bound_name))
else:
scat2 = ax.scatter(x, np.array(bounds), marker=".", )
ax.legend((scat, scat2),
(name, bound_name))
# plt.tight_layout()
else:
ax.legend((scat,), (name,))
f.savefig(file_name)
plt.close()
class BaseBoost(object):
def _collect_probas(self, X):
return np.asarray([clf.predict_proba(X) for clf in self.estimators_generator.estimators_])
def _binary_classification_matrix(self, X):
probas = self._collect_probas(X)
predicted_labels = np.argmax(probas, axis=2)
predicted_labels[predicted_labels == 0] = -1
values = np.max(probas, axis=2)
return (predicted_labels * values).T
def _initialize_alphas(self, n_examples):
raise NotImplementedError("Alpha weights initialization function is not implemented.")
def check_opposed_voters(self, ):
nb_opposed = 0
oppposed = []
for column in self.classification_matrix[:, self.chosen_columns_].transpose():
for chosen_col in self.chosen_columns_:
if (-column.reshape((self.n_total_examples, 1)) == self.classification_matrix[:, chosen_col].reshape((self.n_total_examples, 1))).all():
nb_opposed+=1
break
return int(nb_opposed/2)
def getInterpretBase(classifier, directory, classifier_name, weights,
break_cause=" the dual constrail was not violated"):
interpretString = "\t "+classifier_name+" permformed classification with weights : \n"
# weights_sort = np.argsort(-weights)
weights_sort = np.arange(weights.shape[0])
interpretString += np.array2string(weights[weights_sort], precision=4, separator=',', suppress_small=True)
interpretString += "\n \t It generated {} columns by attributes and used {} iterations to converge, and selected {} couple(s) of opposed voters".format(classifier.n_stumps,
len(weights_sort), classifier.nb_opposed_voters)
if max(weights) > 0.50:
interpretString += "\n \t The vote is useless in this context : voter nb {} is a dictator of weight > 0.50".format(classifier.chosen_columns_[np.argmax(np.array(weights))])
if len(weights_sort) == classifier.n_max_iterations or len(weights) == classifier.n_total_hypotheses_:
if len(weights) == classifier.n_max_iterations:
interpretString += ", and used all available iterations, "
else:
interpretString += "."
if len(weights) == classifier.n_total_hypotheses_:
interpretString += ", and all the voters have been used."
else:
interpretString += "."
else:
pass
# interpretString += ", and the loop was broken because "+break_cause
interpretString += "\n\t Selected voters : \n"
interpretString += np.array2string(np.array(classifier.chosen_columns_)[weights_sort])
interpretString += "\n\t Trained in "+str(datetime.timedelta(seconds=classifier.train_time))+" and predicted in "+str(datetime.timedelta(seconds=classifier.predict_time))+"."
interpretString += "\n\t Selected columns : \n"
interpretString += np.array2string(classifier.classification_matrix[:, classifier.chosen_columns_], precision=4,
separator=',', suppress_small=True)
np.savetxt(directory + "voters.csv", classifier.classification_matrix[:, classifier.chosen_columns_], delimiter=',')
np.savetxt(directory + "weights.csv", classifier.weights_, delimiter=',')
np.savetxt(directory + "times.csv", np.array([classifier.train_time, classifier.predict_time]), delimiter=',')
np.savetxt(directory + "sparsity.csv", np.array([len(weights_sort)]), delimiter=',')
get_accuracy_graph(classifier.train_metrics, classifier_name, directory + 'metrics.png', classifier.plotted_metric, classifier.bounds, "Boosting bound")
return interpretString