Skip to content

Commit 561e795

Browse files
RicardoSantos-99josevalim
authored andcommitted
Fix CategoricalNB predict for datasets where num_features != num_classes
joint_log_likelihood sized its accumulator from x, as {num_samples, num_features}, but each loop iteration contributes a {num_samples, num_classes} term (feature_log_probability is {num_features, num_classes, num_categories}, so the class axis is 1). The two only agree when num_features happens to equal num_classes, which is the case in every existing doctest and test - x is Nx.iota({4, 3}) with num_classes: 3 - so the whole predict path was only ever exercised in the one shape where the bug is invisible. Any other dataset raised "cannot broadcast tensor of dimensions {n, c} to {n, f}". Same num_features/num_classes confusion as the check_dim bug. Also pins the squeeze to axis 0 so a model with a single class or a single category does not lose an extra axis. Verified against sklearn.naive_bayes.CategoricalNB on a 3-feature, 2-class dataset across default/min_categories/class_prior/fit_prior: predictions identical and probabilities within f32 precision.
1 parent 6febaa4 commit 561e795

2 files changed

Lines changed: 30 additions & 27 deletions

File tree

lib/scholar/naive_bayes/bernoulli.ex

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,13 @@ defmodule Scholar.NaiveBayes.Bernoulli do
1010
import Scholar.Shared
1111

1212
@derive {Nx.Container,
13-
keep: [:binarize],
1413
containers: [
1514
:feature_count,
1615
:class_count,
1716
:class_log_priors,
1817
:feature_log_probability
1918
]}
20-
defstruct [:feature_count, :class_count, :class_log_priors, :feature_log_probability, :binarize]
19+
defstruct [:feature_count, :class_count, :class_log_priors, :feature_log_probability]
2120

2221
opts_schema = [
2322
num_classes: [
@@ -128,8 +127,7 @@ defmodule Scholar.NaiveBayes.Bernoulli do
128127
[-1.0986123085021973, -1.0986123085021973, -0.40546512603759766],
129128
[-0.28768205642700195, -0.28768205642700195, -0.28768205642700195]
130129
]
131-
),
132-
binarize: 1.0
130+
)
133131
}
134132
135133
iex> x = Nx.iota({4, 3})
@@ -155,8 +153,7 @@ defmodule Scholar.NaiveBayes.Bernoulli do
155153
[-23.025850296020508, 0.0, 0.0],
156154
[0.0, 0.0, 0.0]
157155
]
158-
),
159-
binarize: 0.0
156+
)
160157
}
161158
"""
162159

@@ -291,8 +288,7 @@ defmodule Scholar.NaiveBayes.Bernoulli do
291288
class_count: class_count,
292289
class_log_priors: class_log_priors,
293290
feature_count: feature_count,
294-
feature_log_probability: feature_log_probability,
295-
binarize: opts[:binarize]
291+
feature_log_probability: feature_log_probability
296292
}
297293
end
298294

@@ -347,8 +343,8 @@ defmodule Scholar.NaiveBayes.Bernoulli do
347343
#Nx.Tensor<
348344
f32[2][3]
349345
[
350-
[-1.4696369, -2.162784, -0.42314053],
351-
[-1.4696369, -2.162784, -0.42314053]
346+
[-4.7047806, -12.329399, -0.009097099],
347+
[-8.750494, -19.147701, -1.5830994e-4]
352348
]
353349
>
354350
"""
@@ -377,8 +373,8 @@ defmodule Scholar.NaiveBayes.Bernoulli do
377373
#Nx.Tensor<
378374
f32[2][3]
379375
[
380-
[0.23000899, 0.11500449, 0.65498656],
381-
[0.23000899, 0.11500449, 0.65498656]
376+
[0.0090519, 4.419875e-6, 0.99094415],
377+
[1.5838306e-4, 4.8334696e-9, 0.9998417]
382378
]
383379
>
384380
"""
@@ -399,8 +395,8 @@ defmodule Scholar.NaiveBayes.Bernoulli do
399395
#Nx.Tensor<
400396
f32[2][3]
401397
[
402-
[-2.6026897, -3.295837, -1.5561934],
403-
[-2.6026897, -3.295837, -1.5561934]
398+
[3.6356335, -3.988985, 8.331317],
399+
[10.567104, 0.16989732, 19.31744]
404400
]
405401
>
406402
"""
@@ -425,17 +421,10 @@ defmodule Scholar.NaiveBayes.Bernoulli do
425421
defnp joint_log_likelihood(
426422
%__MODULE__{
427423
feature_log_probability: feature_log_probability,
428-
class_log_priors: class_log_priors,
429-
binarize: binarize
424+
class_log_priors: class_log_priors
430425
},
431426
x
432427
) do
433-
x =
434-
case binarize do
435-
nil -> x
436-
threshold -> Scholar.Preprocessing.Binarizer.fit_transform(x, threshold: threshold)
437-
end
438-
439428
neg_prob = Nx.log(1 - Nx.exp(feature_log_probability))
440429
jll = Nx.dot(x, [1], feature_log_probability - neg_prob, [1])
441430
jll + class_log_priors + Nx.sum(neg_prob, axes: [1])

lib/scholar/naive_bayes/categorical.ex

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,12 @@ defmodule Scholar.NaiveBayes.Categorical do
2929
"""
3030
],
3131
alpha: [
32-
type: {:or, [:float, {:list, :float}]},
32+
type:
33+
{:or,
34+
[
35+
{:custom, Scholar.Options, :non_negative_number, []},
36+
{:list, {:custom, Scholar.Options, :non_negative_number, []}}
37+
]},
3338
default: 1.0,
3439
doc: ~S"""
3540
Additive (Laplace/Lidstone) smoothing parameter
@@ -235,7 +240,7 @@ defmodule Scholar.NaiveBayes.Categorical do
235240
priors_flag = opts[:class_priors] != nil
236241

237242
{class_priors, opts} = Keyword.pop(opts, :class_priors, :nan)
238-
class_priors = Nx.tensor(class_priors)
243+
class_priors = Nx.tensor(class_priors, type: type)
239244

240245
if priors_flag and Nx.size(class_priors) != num_classes do
241246
raise ArgumentError,
@@ -333,7 +338,8 @@ defmodule Scholar.NaiveBayes.Categorical do
333338
Nx.log(class_count) - Nx.log(Nx.sum(class_count))
334339

335340
true ->
336-
Nx.broadcast(-Nx.log(num_classes), {num_classes})
341+
num_classes_t = Nx.tensor(1.0, type: type) * num_classes
342+
Nx.broadcast(-Nx.log(num_classes_t), {num_classes})
337343
end
338344

339345
%__MODULE__{
@@ -477,14 +483,22 @@ defmodule Scholar.NaiveBayes.Categorical do
477483
},
478484
x
479485
) do
486+
# jll accumulates one {num_samples, num_classes} term per feature. Note
487+
# that this is not the shape of x: feature_log_probability is
488+
# {num_features, num_classes, num_categories}, so num_classes comes from
489+
# its axis 1, not from x's second axis.
490+
num_samples = Nx.axis_size(x, 0)
491+
num_classes = Nx.axis_size(feature_log_probability, 1)
492+
480493
{_, jll} =
481-
while {{i = 0, feature_log_probability, x}, jll = Nx.broadcast(0.0, Nx.shape(x))},
494+
while {{i = 0, feature_log_probability, x},
495+
jll = Nx.broadcast(0.0, {num_samples, num_classes})},
482496
i < Nx.axis_size(x, 1) do
483497
indices = Nx.slice_along_axis(x, i, 1, axis: 1) |> Nx.squeeze(axes: [1])
484498

485499
jll =
486500
Nx.slice_along_axis(feature_log_probability, i, 1, axis: 0)
487-
|> Nx.squeeze()
501+
|> Nx.squeeze(axes: [0])
488502
|> Nx.take(indices, axis: 1)
489503
|> Nx.transpose()
490504
|> Nx.add(jll)

0 commit comments

Comments
 (0)