Skip to content

Commit 5cd99c7

Browse files
authored
Merge pull request #4767 from PrimozGodec/fix-softmax
[FIX] Fix and update Softmax regression learner
2 parents efbe9ef + ec21f13 commit 5cd99c7

File tree

4 files changed

+48
-57
lines changed

4 files changed

+48
-57
lines changed

Orange/base.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -206,15 +206,12 @@ class Model(Reprable):
206206
ValueProbs = 2
207207

208208
def __init__(self, domain=None, original_domain=None):
209-
if isinstance(self, Learner):
210-
domain = None
211-
elif domain is None:
212-
raise ValueError("unspecified domain")
213209
self.domain = domain
214210
if original_domain is not None:
215211
self.original_domain = original_domain
216212
else:
217213
self.original_domain = domain
214+
self.used_vals = None
218215

219216
def predict(self, X):
220217
if type(self).predict_storage is Model.predict_storage:
@@ -383,6 +380,30 @@ def one_hot_probs(value):
383380
probs[:, i, :] = one_hot(value[:, i])
384381
return probs
385382

383+
def extend_probabilities(probs):
384+
"""
385+
Since SklModels and models implementing `fit` and not `fit_storage`
386+
do not guarantee correct prediction dimensionality, extend
387+
dimensionality of probabilities when it does not match the number
388+
of values in the domain.
389+
"""
390+
class_vars = self.domain.class_vars
391+
max_values = max(len(cv.values) for cv in class_vars)
392+
if max_values == probs.shape[-1]:
393+
return probs
394+
395+
if not self.supports_multiclass:
396+
probs = probs[:, np.newaxis, :]
397+
398+
probs_ext = np.zeros((len(probs), len(class_vars), max_values))
399+
for c, used_vals in enumerate(self.used_vals):
400+
for i, cv in enumerate(used_vals):
401+
probs_ext[:, c, cv] = probs[:, c, i]
402+
403+
if not self.supports_multiclass:
404+
probs_ext = probs_ext[:, 0, :]
405+
return probs_ext
406+
386407
def fix_dim(x):
387408
return x[0] if one_d else x
388409

@@ -439,6 +460,7 @@ def fix_dim(x):
439460
if probs is None and (ret != Model.Value or backmappers is not None):
440461
probs = one_hot_probs(value)
441462
if probs is not None:
463+
probs = extend_probabilities(probs)
442464
probs = self.backmap_probs(probs, n_values, backmappers)
443465
if ret != Model.Probs:
444466
if value is None:

Orange/classification/base_classification.py

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import numpy as np
2-
31
from Orange.base import Learner, Model, SklLearner, SklModel
42

53
__all__ = ["LearnerClassification", "ModelClassification",
@@ -18,26 +16,7 @@ class ModelClassification(Model):
1816

1917

2018
class SklModelClassification(SklModel, ModelClassification):
21-
def predict(self, X):
22-
prediction = super().predict(X)
23-
if not isinstance(prediction, tuple):
24-
return prediction
25-
values, probs = prediction
26-
27-
class_vars = self.domain.class_vars
28-
max_values = max(len(cv.values) for cv in class_vars)
29-
if max_values == probs.shape[-1]:
30-
return values, probs
31-
32-
if not self.supports_multiclass:
33-
probs = probs[:, np.newaxis, :]
34-
probs_ext = np.zeros((len(probs), len(class_vars), max_values))
35-
for c, used_vals in enumerate(self.used_vals):
36-
for i, cv in enumerate(used_vals):
37-
probs_ext[:, c, cv] = probs[:, c, i]
38-
if not self.supports_multiclass:
39-
probs_ext = probs_ext[:, 0, :]
40-
return values, probs_ext
19+
pass
4120

4221

4322
class SklLearnerClassification(SklLearner, LearnerClassification):

Orange/classification/softmax_regression.py

Lines changed: 21 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,10 @@ class SoftmaxRegressionLearner(Learner):
2929
parameters to be smaller.
3030
3131
preprocessors : list, optional
32-
Preprocessors are applied to data before training or testing. Default preprocessors:
33-
Defaults to
34-
`[RemoveNaNClasses(), RemoveNaNColumns(), Impute(), Continuize(), Normalize()]`
32+
Preprocessors are applied to data before training or testing. Default
33+
preprocessors:
34+
`[RemoveNaNClasses(), RemoveNaNColumns(), Impute(), Continuize(),
35+
Normalize()]`
3536
3637
- remove columns with all values as NaN
3738
- replace NaN values with suitable values
@@ -52,53 +53,55 @@ def __init__(self, lambda_=1.0, preprocessors=None, **fmin_args):
5253
super().__init__(preprocessors=preprocessors)
5354
self.lambda_ = lambda_
5455
self.fmin_args = fmin_args
56+
self.num_classes = None
5557

56-
def cost_grad(self, Theta_flat, X, Y):
57-
Theta = Theta_flat.reshape((self.num_classes, X.shape[1]))
58+
def cost_grad(self, theta_flat, X, Y):
59+
theta = theta_flat.reshape((self.num_classes, X.shape[1]))
5860

59-
M = X.dot(Theta.T)
61+
M = X.dot(theta.T)
6062
P = np.exp(M - np.max(M, axis=1)[:, None])
6163
P /= np.sum(P, axis=1)[:, None]
6264

6365
cost = -np.sum(np.log(P) * Y)
64-
cost += self.lambda_ * Theta_flat.dot(Theta_flat) / 2.0
66+
cost += self.lambda_ * theta_flat.dot(theta_flat) / 2.0
6567
cost /= X.shape[0]
6668

6769
grad = X.T.dot(P - Y).T
68-
grad += self.lambda_ * Theta
70+
grad += self.lambda_ * theta
6971
grad /= X.shape[0]
7072

7173
return cost, grad.ravel()
7274

73-
def fit(self, X, y, W):
74-
if len(y.shape) > 1:
75+
def fit(self, X, Y, W=None):
76+
if len(Y.shape) > 1:
7577
raise ValueError('Softmax regression does not support '
7678
'multi-label classification')
7779

78-
if np.isnan(np.sum(X)) or np.isnan(np.sum(y)):
80+
if np.isnan(np.sum(X)) or np.isnan(np.sum(Y)):
7981
raise ValueError('Softmax regression does not support '
8082
'unknown values')
8183

8284
X = np.hstack((X, np.ones((X.shape[0], 1))))
8385

84-
self.num_classes = np.unique(y).size
85-
Y = np.eye(self.num_classes)[y.ravel().astype(int)]
86+
self.num_classes = np.unique(Y).size
87+
Y = np.eye(self.num_classes)[Y.ravel().astype(int)]
8688

8789
theta = np.zeros(self.num_classes * X.shape[1])
8890
theta, j, ret = fmin_l_bfgs_b(self.cost_grad, theta,
8991
args=(X, Y), **self.fmin_args)
90-
Theta = theta.reshape((self.num_classes, X.shape[1]))
92+
theta = theta.reshape((self.num_classes, X.shape[1]))
9193

92-
return SoftmaxRegressionModel(Theta)
94+
return SoftmaxRegressionModel(theta)
9395

9496

9597
class SoftmaxRegressionModel(Model):
96-
def __init__(self, Theta):
97-
self.Theta = Theta
98+
def __init__(self, theta):
99+
super().__init__()
100+
self.theta = theta
98101

99102
def predict(self, X):
100103
X = np.hstack((X, np.ones((X.shape[0], 1))))
101-
M = X.dot(self.Theta.T)
104+
M = X.dot(self.theta.T)
102105
P = np.exp(M - np.max(M, axis=1)[:, None])
103106
P /= np.sum(P, axis=1)[:, None]
104107
return P
@@ -119,7 +122,6 @@ def numerical_grad(f, params, e=1e-4):
119122
return grad
120123

121124
d = Orange.data.Table('iris')
122-
m = SoftmaxRegressionLearner(lambda_=1.0)
123125

124126
# gradient check
125127
m = SoftmaxRegressionLearner(lambda_=1.0)
@@ -132,11 +134,3 @@ def numerical_grad(f, params, e=1e-4):
132134

133135
print(ga)
134136
print(gn)
135-
136-
# for lambda_ in [0.1, 0.3, 1, 3, 10]:
137-
# m = SoftmaxRegressionLearner(lambda_=lambda_)
138-
# scores = []
139-
# for tr_ind, te_ind in StratifiedKFold(d.Y.ravel()):
140-
# s = np.mean(m(d[tr_ind])(d[te_ind]) == d[te_ind].Y.ravel())
141-
# scores.append(s)
142-
# print('{:4.1f} {}'.format(lambda_, np.mean(scores)))

Orange/tests/test_classification.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -214,10 +214,6 @@ def test_result_shape(self):
214214
"""
215215
iris = Table('iris')
216216
for learner in all_learners():
217-
# TODO: Softmax Regression will be fixed as a separate PR
218-
if learner is SoftmaxRegressionLearner:
219-
continue
220-
221217
with self.subTest(learner.__name__):
222218
# model trained on only one value (but three in the domain)
223219
try:

0 commit comments

Comments
 (0)