Skip to content

Commit c10dd72

Browse files
use multioutput in simplemultitaskClassifier
1 parent 7ba15d9 commit c10dd72

5 files changed

Lines changed: 70 additions & 34 deletions

File tree

nnetsauce/multitask/simplemultitaskClassifier.py

Lines changed: 64 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from copy import deepcopy
1212
from scipy.special import expit
1313
from sklearn.base import ClassifierMixin
14+
from sklearn.multioutput import MultiOutputRegressor
1415
from sklearn.preprocessing import StandardScaler
1516

1617

@@ -81,6 +82,7 @@ def __init__(
8182
self.type_fit = "classification"
8283
self.obj = obj
8384
self.fit_objs_ = {}
85+
self.multioutput_model_ = None
8486
self.X_scaler_ = StandardScaler()
8587
self.scaled_X_ = None
8688

@@ -115,21 +117,37 @@ def fit(self, X, y, sample_weight=None, **kwargs):
115117
# multitask response
116118
Y = mo.one_hot_encode2(y, self.n_classes_)
117119

120+
# Try MultiOutputRegressor first (more efficient)
118121
try:
119-
for i in range(self.n_classes_):
120-
self.fit_objs_[i] = deepcopy(
121-
self.obj.fit(
122-
self.scaled_X_,
123-
Y[:, i],
124-
sample_weight=sample_weight,
125-
**kwargs
126-
)
127-
)
128-
except Exception as e:
129-
for i in range(self.n_classes_):
130-
self.fit_objs_[i] = deepcopy(
131-
self.obj.fit(self.scaled_X_, Y[:, i], **kwargs)
122+
self.multioutput_model_ = MultiOutputRegressor(deepcopy(self.obj))
123+
try:
124+
self.multioutput_model_.fit(
125+
self.scaled_X_,
126+
Y,
127+
sample_weight=sample_weight,
128+
**kwargs
132129
)
130+
except TypeError:
131+
# If sample_weight not supported, try without it
132+
self.multioutput_model_.fit(self.scaled_X_, Y, **kwargs)
133+
except Exception:
134+
# Fallback: fit separate models for each class
135+
self.multioutput_model_ = None
136+
try:
137+
for i in range(self.n_classes_):
138+
self.fit_objs_[i] = deepcopy(
139+
self.obj.fit(
140+
self.scaled_X_,
141+
Y[:, i],
142+
sample_weight=sample_weight,
143+
**kwargs
144+
)
145+
)
146+
except TypeError:
147+
for i in range(self.n_classes_):
148+
self.fit_objs_[i] = deepcopy(
149+
self.obj.fit(self.scaled_X_, Y[:, i], **kwargs)
150+
)
133151
return self
134152

135153
def predict(self, X, **kwargs):
@@ -169,28 +187,43 @@ def predict_proba(self, X, **kwargs):
169187

170188
shape_X = X.shape
171189

172-
probs = np.zeros((shape_X[0], self.n_classes_))
173-
174-
if len(shape_X) == 1: # one example
175-
n_features = shape_X[0]
176-
177-
new_X = mo.rbind(
178-
X.reshape(1, n_features),
179-
np.ones(n_features).reshape(1, n_features),
180-
)
190+
if self.multioutput_model_ is not None:
191+
# Use MultiOutputRegressor for prediction
192+
if len(shape_X) == 1: # one example
193+
n_features = shape_X[0]
194+
new_X = mo.rbind(
195+
X.reshape(1, n_features),
196+
np.ones(n_features).reshape(1, n_features),
197+
)
198+
Z = self.X_scaler_.transform(new_X, **kwargs)
199+
probs = self.multioutput_model_.predict(Z, **kwargs)[:1, :]
200+
else: # multiple rows
201+
Z = self.X_scaler_.transform(X, **kwargs)
202+
probs = self.multioutput_model_.predict(Z, **kwargs)
203+
else:
204+
# Use separate models for each class
205+
probs = np.zeros((shape_X[0], self.n_classes_))
206+
207+
if len(shape_X) == 1: # one example
208+
n_features = shape_X[0]
209+
210+
new_X = mo.rbind(
211+
X.reshape(1, n_features),
212+
np.ones(n_features).reshape(1, n_features),
213+
)
181214

182-
Z = self.X_scaler_.transform(new_X, **kwargs)
215+
Z = self.X_scaler_.transform(new_X, **kwargs)
183216

184-
# Fallback to standard model
185-
for i in range(self.n_classes_):
186-
probs[:, i] = self.fit_objs_[i].predict(Z, **kwargs)[0]
217+
# Fallback to standard model
218+
for i in range(self.n_classes_):
219+
probs[:, i] = self.fit_objs_[i].predict(Z, **kwargs)[0]
187220

188-
else: # multiple rows
189-
Z = self.X_scaler_.transform(X, **kwargs)
221+
else: # multiple rows
222+
Z = self.X_scaler_.transform(X, **kwargs)
190223

191-
# Fallback to standard model
192-
for i in range(self.n_classes_):
193-
probs[:, i] = self.fit_objs_[i].predict(Z, **kwargs)
224+
# Fallback to standard model
225+
for i in range(self.n_classes_):
226+
probs[:, i] = self.fit_objs_[i].predict(Z, **kwargs)
194227

195228
expit_raw_probs = expit(probs)
196229

nnetsauce/quantile/quantileclassification.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,11 @@ def __init__(self, obj, level=95, scoring="predictions"):
8282
"conformal",
8383
"studentized",
8484
"conformal-studentized",
85-
), "scoring must be 'predictions' or 'residuals'"
85+
), "scoring must be 'predictions' or 'residuals' or 'conformal' or 'studentized' or 'conformal-studentized'"
8686
self.obj = obj
87-
quantileregressor = QuantileRegressor(self.obj)
87+
self.level = level
88+
self.scoring = scoring
89+
quantileregressor = QuantileRegressor(self.obj, self.level, self.scoring)
8890
quantileregressor.predict = partial(
8991
quantileregressor.predict, return_pi=False
9092
)

nnetsauce/quantile/quantileregression.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,9 @@ def __init__(self, obj, level=95, scoring="predictions"):
7575
"conformal",
7676
"studentized",
7777
"conformal-studentized",
78-
), "scoring must be 'predictions' or 'residuals'"
78+
), "scoring must be 'predictions' or 'residuals' or 'conformal' or 'studentized' or 'conformal-studentized'"
7979
self.obj = obj
80+
self.level = level
8081
low_risk_level = (1 - level / 100) / 2
8182
self.quantiles = [low_risk_level, 0.5, 1 - low_risk_level]
8283
self.scoring = scoring

rff_bayesian_demo.png

-323 KB
Binary file not shown.

rff_posterior_samples.png

-136 KB
Binary file not shown.

0 commit comments

Comments
 (0)