Wrapper for sparse, integer linear models.
minimizes norm(X * w - y, 2) + alpha * norm(w, 1)
with integer coefficients in w
Requires installation of a solver for mixed-integer linear programs, e.g. gurobi, mosek, or cplex
Expand source code
'''
Wrapper for sparse, integer linear models.
minimizes norm(X * w - y, 2) + alpha * norm(w, 1)
with integer coefficients in w
Requires installation of a solver for mixed-integer linear programs, e.g. gurobi, mosek, or cplex
'''
from imodels.util.introspection import TextMixin
from imodels.util.introspection import TextMixin
import warnings
import numpy as np
from sklearn.base import BaseEstimator, ClassifierMixin, RegressorMixin
from sklearn.linear_model import LinearRegression, Lasso, LogisticRegression
from sklearn.utils.multiclass import check_classification_targets
from sklearn.utils.validation import check_X_y, check_array, check_is_fitted
from imodels.util.arguments import check_predict_X, set_feature_names_in
from imodels.util.optional_deps import warn_optional_dependency
def _warn_if_rounding_collapsed(coef, rounded):
"""Warn when rounding to integers has destroyed the fitted model.
SLIM's coefficients are integers, so a feature whose natural coefficient is
much smaller than 1 rounds to zero. On data whose columns are on very
different scales that silently removes most of the model: on breast_cancer,
raw features give a below-chance classifier while standardized ones reach
0.99 AUC.
"""
nonzero_before = int(np.count_nonzero(coef))
nonzero_after = int(np.count_nonzero(rounded))
if nonzero_before and nonzero_after * 2 < nonzero_before:
warnings.warn(
f"Rounding coefficients to integers left {nonzero_after} of "
f"{nonzero_before} nonzero, so most of the fitted model was lost. "
"SLIM uses integer coefficients, so features on very different "
"scales collapse when rounded; scale X first (for example with "
"sklearn.preprocessing.StandardScaler)."
)
class SLIMRegressor(TextMixin, RegressorMixin, BaseEstimator):
'''Sparse integer linear model
Parameters
----------
alpha: float
weight for sparsity penalty
'''
def __init__(self, alpha=0.01):
warn_optional_dependency(
'cvxpy', 'SLIMRegressor', fallback='rounding the coefficients of a non-integer fit',
purpose='used to solve the integer program exactly')
self.alpha = alpha
def fit(self, X, y, sample_weight=None):
'''fit a linear model with integer coefficient and L1 regularization.
In case the optimization fails, fit lasso and round coefs.
Parameters
----------
_sample_weight: np.ndarray (n,), optional
weight for each individual sample
'''
set_feature_names_in(self, X)
X, y = check_X_y(X, y)
self.n_features_in_ = X.shape[1]
self.model_ = LinearRegression()
try:
import cvxpy as cp # package for optimization, import here to make it optional
from cvxpy.error import SolverError
# declare the integer-valued optimization variable
w = cp.Variable(X.shape[1], integer=True)
# set up the minimization problem
residuals = X @ w - y
if sample_weight is not None:
residuals = cp.multiply(sample_weight, residuals)
mse = cp.sum_squares(residuals)
l1_penalty = self.alpha * cp.norm(w, 1)
obj = cp.Minimize(mse + l1_penalty)
prob = cp.Problem(obj)
try:
# solve the problem using an appropriate solver
prob.solve()
self.model_.coef_ = w.value.astype(int)
self.model_.intercept_ = 0
except SolverError:
warnings.warn("gurobi, mosek, or cplex solver required for mixed-integer "
"quadratic programming. Rounding non-integer coefficients instead.")
self._fit_backup(X, y, sample_weight)
except ImportError:
warnings.warn("Should install cvxpy with pip install cvxpy. Rounding non-integer "
"coefficients instead.")
self._fit_backup(X, y, sample_weight)
return self
def _fit_backup(self, X, y, sample_weight):
m = Lasso(alpha=self.alpha)
m.fit(X, y, sample_weight=sample_weight)
rounded = np.round(m.coef_).astype(int)
_warn_if_rounding_collapsed(m.coef_, rounded)
self.model_.coef_ = rounded
self.model_.intercept_ = m.intercept_
def predict(self, X):
check_is_fitted(self)
check_predict_X(self, X)
X = check_array(X)
return self.model_.predict(X)
class SLIMClassifier(TextMixin, ClassifierMixin, BaseEstimator):
def __init__(self, alpha=1):
'''Model is initialized during fitting
Parameters
----------
alpha: float
weight for sparsity penalty
'''
warn_optional_dependency(
'cvxpy', 'SLIMClassifier', fallback='rounding the coefficients of a non-integer fit',
purpose='used to solve the integer program exactly')
self.alpha = alpha
def fit(self, X, y, sample_weight=None):
'''fit a logistic model with integer coefficient and L1 regularization.
In case the optimization fails, fit lasso and round coefs.
Parameters
----------
_sample_weight: np.ndarray (n,), optional
weight for each individual sample
'''
set_feature_names_in(self, X)
X, y = check_X_y(X, y)
check_classification_targets(y)
self.n_features_in_ = X.shape[1]
self.classes_, y = np.unique(y, return_inverse=True) # deals with str inputs
self.model_ = LogisticRegression()
self.model_.classes_ = self.classes_
try:
import cvxpy as cp # package for optimization, import here to make it optional
from cvxpy.error import SolverError
# declare the integer-valued optimization variable
w = cp.Variable(X.shape[1], integer=True)
# set up the minimization problem
logits = -X @ w
residuals = cp.multiply(1 - y, logits) - cp.logistic(logits)
if sample_weight is not None:
residuals = cp.multiply(sample_weight, residuals)
celoss = -cp.sum(residuals)
l1_penalty = self.alpha * cp.norm(w, 1)
obj = cp.Minimize(celoss + l1_penalty)
prob = cp.Problem(obj)
try:
# solve the problem using an appropriate solver
prob.solve()
self.model_.coef_ = np.array([w.value.astype(int)])
self.model_.intercept_ = 0
except SolverError:
warnings.warn("mosek solver required for mixed-integer exponential cone "
"programming. Rounding non-integer coefficients instead")
self._fit_backup(X, y, sample_weight)
except ImportError:
warnings.warn("Should install cvxpy with pip install cvxpy. Rounding non-integer "
"coefficients instead.")
self._fit_backup(X, y, sample_weight)
return self
def _fit_backup(self, X, y, sample_weight=None):
m = LogisticRegression(C=1 / self.alpha)
m.fit(X, y, sample_weight=sample_weight)
rounded = np.round(m.coef_).astype(int)
_warn_if_rounding_collapsed(m.coef_, rounded)
self.model_.coef_ = rounded
self.model_.intercept_ = m.intercept_
def predict(self, X):
check_is_fitted(self)
check_predict_X(self, X)
X = check_array(X)
return self.model_.predict(X)
def predict_proba(self, X):
check_is_fitted(self)
check_predict_X(self, X)
X = check_array(X)
return self.model_.predict_proba(X)
Classes
class SLIMClassifier (alpha=1)-
Prints a fitted model as readable text (see imodels.viz.text): its tree, rules, points or shape functions, with the numbers that make each prediction. Unfitted models, and models imodels.viz cannot read, print their parameters as before.
Model is initialized during fitting
Parameters
alpha:float- weight for sparsity penalty
Expand source code
class SLIMClassifier(TextMixin, ClassifierMixin, BaseEstimator): def __init__(self, alpha=1): '''Model is initialized during fitting Parameters ---------- alpha: float weight for sparsity penalty ''' warn_optional_dependency( 'cvxpy', 'SLIMClassifier', fallback='rounding the coefficients of a non-integer fit', purpose='used to solve the integer program exactly') self.alpha = alpha def fit(self, X, y, sample_weight=None): '''fit a logistic model with integer coefficient and L1 regularization. In case the optimization fails, fit lasso and round coefs. Parameters ---------- _sample_weight: np.ndarray (n,), optional weight for each individual sample ''' set_feature_names_in(self, X) X, y = check_X_y(X, y) check_classification_targets(y) self.n_features_in_ = X.shape[1] self.classes_, y = np.unique(y, return_inverse=True) # deals with str inputs self.model_ = LogisticRegression() self.model_.classes_ = self.classes_ try: import cvxpy as cp # package for optimization, import here to make it optional from cvxpy.error import SolverError # declare the integer-valued optimization variable w = cp.Variable(X.shape[1], integer=True) # set up the minimization problem logits = -X @ w residuals = cp.multiply(1 - y, logits) - cp.logistic(logits) if sample_weight is not None: residuals = cp.multiply(sample_weight, residuals) celoss = -cp.sum(residuals) l1_penalty = self.alpha * cp.norm(w, 1) obj = cp.Minimize(celoss + l1_penalty) prob = cp.Problem(obj) try: # solve the problem using an appropriate solver prob.solve() self.model_.coef_ = np.array([w.value.astype(int)]) self.model_.intercept_ = 0 except SolverError: warnings.warn("mosek solver required for mixed-integer exponential cone " "programming. Rounding non-integer coefficients instead") self._fit_backup(X, y, sample_weight) except ImportError: warnings.warn("Should install cvxpy with pip install cvxpy. Rounding non-integer " "coefficients instead.") self._fit_backup(X, y, sample_weight) return self def _fit_backup(self, X, y, sample_weight=None): m = LogisticRegression(C=1 / self.alpha) m.fit(X, y, sample_weight=sample_weight) rounded = np.round(m.coef_).astype(int) _warn_if_rounding_collapsed(m.coef_, rounded) self.model_.coef_ = rounded self.model_.intercept_ = m.intercept_ def predict(self, X): check_is_fitted(self) check_predict_X(self, X) X = check_array(X) return self.model_.predict(X) def predict_proba(self, X): check_is_fitted(self) check_predict_X(self, X) X = check_array(X) return self.model_.predict_proba(X)Ancestors
- TextMixin
- sklearn.base.ClassifierMixin
- sklearn.base.BaseEstimator
- sklearn.utils._estimator_html_repr._HTMLDocumentationLinkMixin
- sklearn.utils._metadata_requests._MetadataRequester
Methods
def fit(self, X, y, sample_weight=None)-
fit a logistic model with integer coefficient and L1 regularization. In case the optimization fails, fit lasso and round coefs.
Parameters
_sample_weight:np.ndarray (n,), optional- weight for each individual sample
Expand source code
def fit(self, X, y, sample_weight=None): '''fit a logistic model with integer coefficient and L1 regularization. In case the optimization fails, fit lasso and round coefs. Parameters ---------- _sample_weight: np.ndarray (n,), optional weight for each individual sample ''' set_feature_names_in(self, X) X, y = check_X_y(X, y) check_classification_targets(y) self.n_features_in_ = X.shape[1] self.classes_, y = np.unique(y, return_inverse=True) # deals with str inputs self.model_ = LogisticRegression() self.model_.classes_ = self.classes_ try: import cvxpy as cp # package for optimization, import here to make it optional from cvxpy.error import SolverError # declare the integer-valued optimization variable w = cp.Variable(X.shape[1], integer=True) # set up the minimization problem logits = -X @ w residuals = cp.multiply(1 - y, logits) - cp.logistic(logits) if sample_weight is not None: residuals = cp.multiply(sample_weight, residuals) celoss = -cp.sum(residuals) l1_penalty = self.alpha * cp.norm(w, 1) obj = cp.Minimize(celoss + l1_penalty) prob = cp.Problem(obj) try: # solve the problem using an appropriate solver prob.solve() self.model_.coef_ = np.array([w.value.astype(int)]) self.model_.intercept_ = 0 except SolverError: warnings.warn("mosek solver required for mixed-integer exponential cone " "programming. Rounding non-integer coefficients instead") self._fit_backup(X, y, sample_weight) except ImportError: warnings.warn("Should install cvxpy with pip install cvxpy. Rounding non-integer " "coefficients instead.") self._fit_backup(X, y, sample_weight) return self def predict(self, X)-
Expand source code
def predict(self, X): check_is_fitted(self) check_predict_X(self, X) X = check_array(X) return self.model_.predict(X) def predict_proba(self, X)-
Expand source code
def predict_proba(self, X): check_is_fitted(self) check_predict_X(self, X) X = check_array(X) return self.model_.predict_proba(X)
class SLIMRegressor (alpha=0.01)-
Sparse integer linear model Parameters
alpha:float- weight for sparsity penalty
Expand source code
class SLIMRegressor(TextMixin, RegressorMixin, BaseEstimator): '''Sparse integer linear model Parameters ---------- alpha: float weight for sparsity penalty ''' def __init__(self, alpha=0.01): warn_optional_dependency( 'cvxpy', 'SLIMRegressor', fallback='rounding the coefficients of a non-integer fit', purpose='used to solve the integer program exactly') self.alpha = alpha def fit(self, X, y, sample_weight=None): '''fit a linear model with integer coefficient and L1 regularization. In case the optimization fails, fit lasso and round coefs. Parameters ---------- _sample_weight: np.ndarray (n,), optional weight for each individual sample ''' set_feature_names_in(self, X) X, y = check_X_y(X, y) self.n_features_in_ = X.shape[1] self.model_ = LinearRegression() try: import cvxpy as cp # package for optimization, import here to make it optional from cvxpy.error import SolverError # declare the integer-valued optimization variable w = cp.Variable(X.shape[1], integer=True) # set up the minimization problem residuals = X @ w - y if sample_weight is not None: residuals = cp.multiply(sample_weight, residuals) mse = cp.sum_squares(residuals) l1_penalty = self.alpha * cp.norm(w, 1) obj = cp.Minimize(mse + l1_penalty) prob = cp.Problem(obj) try: # solve the problem using an appropriate solver prob.solve() self.model_.coef_ = w.value.astype(int) self.model_.intercept_ = 0 except SolverError: warnings.warn("gurobi, mosek, or cplex solver required for mixed-integer " "quadratic programming. Rounding non-integer coefficients instead.") self._fit_backup(X, y, sample_weight) except ImportError: warnings.warn("Should install cvxpy with pip install cvxpy. Rounding non-integer " "coefficients instead.") self._fit_backup(X, y, sample_weight) return self def _fit_backup(self, X, y, sample_weight): m = Lasso(alpha=self.alpha) m.fit(X, y, sample_weight=sample_weight) rounded = np.round(m.coef_).astype(int) _warn_if_rounding_collapsed(m.coef_, rounded) self.model_.coef_ = rounded self.model_.intercept_ = m.intercept_ def predict(self, X): check_is_fitted(self) check_predict_X(self, X) X = check_array(X) return self.model_.predict(X)Ancestors
- TextMixin
- sklearn.base.RegressorMixin
- sklearn.base.BaseEstimator
- sklearn.utils._estimator_html_repr._HTMLDocumentationLinkMixin
- sklearn.utils._metadata_requests._MetadataRequester
Methods
def fit(self, X, y, sample_weight=None)-
fit a linear model with integer coefficient and L1 regularization. In case the optimization fails, fit lasso and round coefs.
Parameters
_sample_weight:np.ndarray (n,), optional- weight for each individual sample
Expand source code
def fit(self, X, y, sample_weight=None): '''fit a linear model with integer coefficient and L1 regularization. In case the optimization fails, fit lasso and round coefs. Parameters ---------- _sample_weight: np.ndarray (n,), optional weight for each individual sample ''' set_feature_names_in(self, X) X, y = check_X_y(X, y) self.n_features_in_ = X.shape[1] self.model_ = LinearRegression() try: import cvxpy as cp # package for optimization, import here to make it optional from cvxpy.error import SolverError # declare the integer-valued optimization variable w = cp.Variable(X.shape[1], integer=True) # set up the minimization problem residuals = X @ w - y if sample_weight is not None: residuals = cp.multiply(sample_weight, residuals) mse = cp.sum_squares(residuals) l1_penalty = self.alpha * cp.norm(w, 1) obj = cp.Minimize(mse + l1_penalty) prob = cp.Problem(obj) try: # solve the problem using an appropriate solver prob.solve() self.model_.coef_ = w.value.astype(int) self.model_.intercept_ = 0 except SolverError: warnings.warn("gurobi, mosek, or cplex solver required for mixed-integer " "quadratic programming. Rounding non-integer coefficients instead.") self._fit_backup(X, y, sample_weight) except ImportError: warnings.warn("Should install cvxpy with pip install cvxpy. Rounding non-integer " "coefficients instead.") self._fit_backup(X, y, sample_weight) return self def predict(self, X)-
Expand source code
def predict(self, X): check_is_fitted(self) check_predict_X(self, X) X = check_array(X) return self.model_.predict(X)