Skip to content

Commit 65b300e

Browse files
Micky774jjerphanthomasjpfanarka204Shao Yang Hong
authored
ENH Adding variable force_alpha to classes in naive_bayes.py (#22269)
Co-authored-by: Julien Jerphanion <git@jjerphan.xyz> Co-authored-by: Thomas J. Fan <thomasjpfan@gmail.com> Co-authored-by: arka204 <kmichalik204@gmail.com> Co-authored-by: Shao Yang Hong <shaoyang.hong@ninjavan.co> Co-authored-by: Shao Yang Hong <hongsy2006@gmail.com>
1 parent 2e49193 commit 65b300e

File tree

6 files changed

+206
-29
lines changed

6 files changed

+206
-29
lines changed

doc/whats_new/v1.2.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,18 @@ Changelog
282282
:pr:`10805` by :user:`Mathias Andersen <MrMathias>` and
283283
:pr:`23471` by :user:`Meekail Zain <micky774>`
284284

285+
:mod:`sklearn.naive_bayes`
286+
..........................
287+
288+
- |Enhancement| A new parameter `force_alpha` was added to
289+
:class:`naive_bayes.BernoulliNB`, :class:`naive_bayes.ComplementNB`,
290+
:class:`naive_bayes.CategoricalNB`, and :class:`naive_bayes.MultinomialNB`,
291+
allowing user to set parameter alpha to a very small number, greater or equal
292+
0, which was earlier automatically changed to `1e-10` instead.
293+
:pr:`16747` by :user:`arka204`,
294+
:pr:`18805` by :user:`hongshaoyang`,
295+
:pr:`22269` by :user:`Meekail Zain <micky774>`.
296+
285297
Code and Documentation Contributors
286298
-----------------------------------
287299

sklearn/naive_bayes.py

Lines changed: 122 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
from .utils.multiclass import _check_partial_fit_first_call
3131
from .utils.validation import check_is_fitted, check_non_negative
3232
from .utils.validation import _check_sample_weight
33-
from .utils._param_validation import Interval
33+
from .utils._param_validation import Interval, Hidden, StrOptions
3434

3535
__all__ = [
3636
"BernoulliNB",
@@ -549,12 +549,14 @@ class _BaseDiscreteNB(_BaseNB):
549549
"alpha": [Interval(Real, 0, None, closed="left"), "array-like"],
550550
"fit_prior": ["boolean"],
551551
"class_prior": ["array-like", None],
552+
"force_alpha": ["boolean", Hidden(StrOptions({"warn"}))],
552553
}
553554

554-
def __init__(self, alpha=1.0, fit_prior=True, class_prior=None):
555+
def __init__(self, alpha=1.0, fit_prior=True, class_prior=None, force_alpha="warn"):
555556
self.alpha = alpha
556557
self.fit_prior = fit_prior
557558
self.class_prior = class_prior
559+
self.force_alpha = force_alpha
558560

559561
@abstractmethod
560562
def _count(self, X, Y):
@@ -622,22 +624,34 @@ def _check_alpha(self):
622624
alpha = (
623625
np.asarray(self.alpha) if not isinstance(self.alpha, Real) else self.alpha
624626
)
627+
alpha_min = np.min(alpha)
625628
if isinstance(alpha, np.ndarray):
626629
if not alpha.shape[0] == self.n_features_in_:
627630
raise ValueError(
628631
"When alpha is an array, it should contains `n_features`. "
629632
f"Got {alpha.shape[0]} elements instead of {self.n_features_in_}."
630633
)
631634
# check that all alpha are positive
632-
if np.min(alpha) < 0:
635+
if alpha_min < 0:
633636
raise ValueError("All values in alpha must be greater than 0.")
634-
alpha_min = 1e-10
635-
if np.min(alpha) < alpha_min:
637+
alpha_lower_bound = 1e-10
638+
# TODO(1.4): Replace w/ deprecation of self.force_alpha
639+
# See gh #22269
640+
_force_alpha = self.force_alpha
641+
if _force_alpha == "warn" and alpha_min < alpha_lower_bound:
642+
_force_alpha = False
643+
warnings.warn(
644+
"The default value for `force_alpha` will change to `True` in 1.4. To"
645+
" suppress this warning, manually set the value of `force_alpha`.",
646+
FutureWarning,
647+
)
648+
if alpha_min < alpha_lower_bound and not _force_alpha:
636649
warnings.warn(
637650
"alpha too small will result in numeric errors, setting alpha ="
638-
f" {alpha_min:.1e}"
651+
f" {alpha_lower_bound:.1e}. Use `force_alpha=True` to keep alpha"
652+
" unchanged."
639653
)
640-
return np.maximum(alpha, alpha_min)
654+
return np.maximum(alpha, alpha_lower_bound)
641655
return alpha
642656

643657
def partial_fit(self, X, y, classes=None, sample_weight=None):
@@ -812,7 +826,16 @@ class MultinomialNB(_BaseDiscreteNB):
812826
----------
813827
alpha : float or array-like of shape (n_features,), default=1.0
814828
Additive (Laplace/Lidstone) smoothing parameter
815-
(0 for no smoothing).
829+
(set alpha=0 and force_alpha=True, for no smoothing).
830+
831+
force_alpha : bool, default=False
832+
If False and alpha is less than 1e-10, it will set alpha to
833+
1e-10. If True, alpha will remain unchanged. This may cause
834+
numerical errors if alpha is too close to 0.
835+
836+
.. versionadded:: 1.2
837+
.. deprecated:: 1.2
838+
The default value of `force_alpha` will change to `True` in v1.4.
816839
817840
fit_prior : bool, default=True
818841
Whether to learn class prior probabilities or not.
@@ -881,15 +904,22 @@ class MultinomialNB(_BaseDiscreteNB):
881904
>>> X = rng.randint(5, size=(6, 100))
882905
>>> y = np.array([1, 2, 3, 4, 5, 6])
883906
>>> from sklearn.naive_bayes import MultinomialNB
884-
>>> clf = MultinomialNB()
907+
>>> clf = MultinomialNB(force_alpha=True)
885908
>>> clf.fit(X, y)
886-
MultinomialNB()
909+
MultinomialNB(force_alpha=True)
887910
>>> print(clf.predict(X[2:3]))
888911
[3]
889912
"""
890913

891-
def __init__(self, *, alpha=1.0, fit_prior=True, class_prior=None):
892-
super().__init__(alpha=alpha, fit_prior=fit_prior, class_prior=class_prior)
914+
def __init__(
915+
self, *, alpha=1.0, force_alpha="warn", fit_prior=True, class_prior=None
916+
):
917+
super().__init__(
918+
alpha=alpha,
919+
fit_prior=fit_prior,
920+
class_prior=class_prior,
921+
force_alpha=force_alpha,
922+
)
893923

894924
def _more_tags(self):
895925
return {"requires_positive_X": True}
@@ -928,7 +958,17 @@ class ComplementNB(_BaseDiscreteNB):
928958
Parameters
929959
----------
930960
alpha : float or array-like of shape (n_features,), default=1.0
931-
Additive (Laplace/Lidstone) smoothing parameter (0 for no smoothing).
961+
Additive (Laplace/Lidstone) smoothing parameter
962+
(set alpha=0 and force_alpha=True, for no smoothing).
963+
964+
force_alpha : bool, default=False
965+
If False and alpha is less than 1e-10, it will set alpha to
966+
1e-10. If True, alpha will remain unchanged. This may cause
967+
numerical errors if alpha is too close to 0.
968+
969+
.. versionadded:: 1.2
970+
.. deprecated:: 1.2
971+
The default value of `force_alpha` will change to `True` in v1.4.
932972
933973
fit_prior : bool, default=True
934974
Only used in edge case with a single class in the training set.
@@ -1005,9 +1045,9 @@ class ComplementNB(_BaseDiscreteNB):
10051045
>>> X = rng.randint(5, size=(6, 100))
10061046
>>> y = np.array([1, 2, 3, 4, 5, 6])
10071047
>>> from sklearn.naive_bayes import ComplementNB
1008-
>>> clf = ComplementNB()
1048+
>>> clf = ComplementNB(force_alpha=True)
10091049
>>> clf.fit(X, y)
1010-
ComplementNB()
1050+
ComplementNB(force_alpha=True)
10111051
>>> print(clf.predict(X[2:3]))
10121052
[3]
10131053
"""
@@ -1017,8 +1057,21 @@ class ComplementNB(_BaseDiscreteNB):
10171057
"norm": ["boolean"],
10181058
}
10191059

1020-
def __init__(self, *, alpha=1.0, fit_prior=True, class_prior=None, norm=False):
1021-
super().__init__(alpha=alpha, fit_prior=fit_prior, class_prior=class_prior)
1060+
def __init__(
1061+
self,
1062+
*,
1063+
alpha=1.0,
1064+
force_alpha="warn",
1065+
fit_prior=True,
1066+
class_prior=None,
1067+
norm=False,
1068+
):
1069+
super().__init__(
1070+
alpha=alpha,
1071+
force_alpha=force_alpha,
1072+
fit_prior=fit_prior,
1073+
class_prior=class_prior,
1074+
)
10221075
self.norm = norm
10231076

10241077
def _more_tags(self):
@@ -1064,7 +1117,16 @@ class BernoulliNB(_BaseDiscreteNB):
10641117
----------
10651118
alpha : float or array-like of shape (n_features,), default=1.0
10661119
Additive (Laplace/Lidstone) smoothing parameter
1067-
(0 for no smoothing).
1120+
(set alpha=0 and force_alpha=True, for no smoothing).
1121+
1122+
force_alpha : bool, default=False
1123+
If False and alpha is less than 1e-10, it will set alpha to
1124+
1e-10. If True, alpha will remain unchanged. This may cause
1125+
numerical errors if alpha is too close to 0.
1126+
1127+
.. versionadded:: 1.2
1128+
.. deprecated:: 1.2
1129+
The default value of `force_alpha` will change to `True` in v1.4.
10681130
10691131
binarize : float or None, default=0.0
10701132
Threshold for binarizing (mapping to booleans) of sample features.
@@ -1144,9 +1206,9 @@ class BernoulliNB(_BaseDiscreteNB):
11441206
>>> X = rng.randint(5, size=(6, 100))
11451207
>>> Y = np.array([1, 2, 3, 4, 4, 5])
11461208
>>> from sklearn.naive_bayes import BernoulliNB
1147-
>>> clf = BernoulliNB()
1209+
>>> clf = BernoulliNB(force_alpha=True)
11481210
>>> clf.fit(X, Y)
1149-
BernoulliNB()
1211+
BernoulliNB(force_alpha=True)
11501212
>>> print(clf.predict(X[2:3]))
11511213
[3]
11521214
"""
@@ -1156,8 +1218,21 @@ class BernoulliNB(_BaseDiscreteNB):
11561218
"binarize": [None, Interval(Real, 0, None, closed="left")],
11571219
}
11581220

1159-
def __init__(self, *, alpha=1.0, binarize=0.0, fit_prior=True, class_prior=None):
1160-
super().__init__(alpha=alpha, fit_prior=fit_prior, class_prior=class_prior)
1221+
def __init__(
1222+
self,
1223+
*,
1224+
alpha=1.0,
1225+
force_alpha="warn",
1226+
binarize=0.0,
1227+
fit_prior=True,
1228+
class_prior=None,
1229+
):
1230+
super().__init__(
1231+
alpha=alpha,
1232+
fit_prior=fit_prior,
1233+
class_prior=class_prior,
1234+
force_alpha=force_alpha,
1235+
)
11611236
self.binarize = binarize
11621237

11631238
def _check_X(self, X):
@@ -1219,7 +1294,16 @@ class CategoricalNB(_BaseDiscreteNB):
12191294
----------
12201295
alpha : float, default=1.0
12211296
Additive (Laplace/Lidstone) smoothing parameter
1222-
(0 for no smoothing).
1297+
(set alpha=0 and force_alpha=True, for no smoothing).
1298+
1299+
force_alpha : bool, default=False
1300+
If False and alpha is less than 1e-10, it will set alpha to
1301+
1e-10. If True, alpha will remain unchanged. This may cause
1302+
numerical errors if alpha is too close to 0.
1303+
1304+
.. versionadded:: 1.2
1305+
.. deprecated:: 1.2
1306+
The default value of `force_alpha` will change to `True` in v1.4.
12231307
12241308
fit_prior : bool, default=True
12251309
Whether to learn class prior probabilities or not.
@@ -1301,9 +1385,9 @@ class CategoricalNB(_BaseDiscreteNB):
13011385
>>> X = rng.randint(5, size=(6, 100))
13021386
>>> y = np.array([1, 2, 3, 4, 5, 6])
13031387
>>> from sklearn.naive_bayes import CategoricalNB
1304-
>>> clf = CategoricalNB()
1388+
>>> clf = CategoricalNB(force_alpha=True)
13051389
>>> clf.fit(X, y)
1306-
CategoricalNB()
1390+
CategoricalNB(force_alpha=True)
13071391
>>> print(clf.predict(X[2:3]))
13081392
[3]
13091393
"""
@@ -1319,9 +1403,20 @@ class CategoricalNB(_BaseDiscreteNB):
13191403
}
13201404

13211405
def __init__(
1322-
self, *, alpha=1.0, fit_prior=True, class_prior=None, min_categories=None
1406+
self,
1407+
*,
1408+
alpha=1.0,
1409+
force_alpha="warn",
1410+
fit_prior=True,
1411+
class_prior=None,
1412+
min_categories=None,
13231413
):
1324-
super().__init__(alpha=alpha, fit_prior=fit_prior, class_prior=class_prior)
1414+
super().__init__(
1415+
alpha=alpha,
1416+
force_alpha=force_alpha,
1417+
fit_prior=fit_prior,
1418+
class_prior=class_prior,
1419+
)
13251420
self.min_categories = min_categories
13261421

13271422
def fit(self, X, y, sample_weight=None):

sklearn/tests/test_calibration.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def test_calibration(data, method, ensemble):
7171
X_test, y_test = X[n_samples:], y[n_samples:]
7272

7373
# Naive-Bayes
74-
clf = MultinomialNB().fit(X_train, y_train, sample_weight=sw_train)
74+
clf = MultinomialNB(force_alpha=True).fit(X_train, y_train, sample_weight=sw_train)
7575
prob_pos_clf = clf.predict_proba(X_test)[:, 1]
7676

7777
cal_clf = CalibratedClassifierCV(clf, cv=y.size + 1, ensemble=ensemble)
@@ -322,7 +322,7 @@ def test_calibration_prefit():
322322
X_test, y_test = X[2 * n_samples :], y[2 * n_samples :]
323323

324324
# Naive-Bayes
325-
clf = MultinomialNB()
325+
clf = MultinomialNB(force_alpha=True)
326326
# Check error if clf not prefit
327327
unfit_clf = CalibratedClassifierCV(clf, cv="prefit")
328328
with pytest.raises(NotFittedError):

sklearn/tests/test_docstring_parameters.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,14 @@ def test_fit_docstring_attributes(name, Estimator):
268268
est.set_params(n_init="auto")
269269

270270
# TODO(1.4): TO BE REMOVED for 1.4 (avoid FutureWarning)
271+
if Estimator.__name__ in (
272+
"MultinomialNB",
273+
"ComplementNB",
274+
"BernoulliNB",
275+
"CategoricalNB",
276+
):
277+
est.set_params(force_alpha=True)
278+
271279
if Estimator.__name__ == "QuantileRegressor":
272280
solver = "highs" if sp_version >= parse_version("1.6.0") else "interior-point"
273281
est.set_params(solver=solver)

sklearn/tests/test_multiclass.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@
4141
from sklearn import datasets
4242
from sklearn.datasets import load_breast_cancer
4343

44+
msg = "The default value for `force_alpha` will change"
45+
pytestmark = pytest.mark.filterwarnings(f"ignore:{msg}:FutureWarning")
46+
4447
iris = datasets.load_iris()
4548
rng = np.random.RandomState(0)
4649
perm = rng.permutation(iris.target.size)

0 commit comments

Comments
 (0)