Skip to content

Commit f527157

Browse files
committed
Remove hardcoded softmax
1 parent 303d681 commit f527157

2 files changed

Lines changed: 97 additions & 9 deletions

File tree

tests/test_model.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,22 @@
88
import tn4ml.metrics as metrics
99
from tn4ml.embeddings import TrigonometricEmbedding
1010
from tn4ml.initializers import randn
11-
from tn4ml.models.model import _batch_iterator
11+
from tn4ml.models.model import Model, _batch_iterator
1212
from tn4ml.models.mps import MPS_initialize
1313
from tn4ml.models.smpo import SMPO_initialize
1414
from tn4ml.util import TrainingType
1515

1616
jax.config.update("jax_enable_x64", True)
1717

1818

19+
class _IdentityAccuracyModel(Model):
20+
def __init__(self):
21+
self.device = ("cpu", 0)
22+
23+
def predict(self, sample, embedding=None, return_tn=False, normalize=False):
24+
return sample
25+
26+
1927
# --- nparams ---
2028

2129

@@ -294,6 +302,49 @@ def test_predict_input_too_short():
294302
model.predict(sample)
295303

296304

305+
# --- accuracy ---
306+
307+
308+
def test_accuracy_uses_raw_outputs_by_default():
309+
model = _IdentityAccuracyModel()
310+
data = np.array([[0.1, 0.9], [0.8, 0.2]])
311+
targets = np.array([[0, 1], [1, 0]])
312+
313+
accuracy = model.accuracy(data, targets, batch_size=2)
314+
315+
assert accuracy == pytest.approx(1.0)
316+
317+
318+
def test_accuracy_accepts_score_transform():
319+
model = _IdentityAccuracyModel()
320+
data = np.array([[0.1, 0.9], [0.8, 0.2]])
321+
targets = np.array([[0, 1], [1, 0]])
322+
323+
accuracy = model.accuracy(
324+
data,
325+
targets,
326+
batch_size=2,
327+
accuracy_fn=lambda scores: scores[:, ::-1],
328+
)
329+
330+
assert accuracy == pytest.approx(0.0)
331+
332+
333+
def test_accuracy_accepts_label_transform_and_integer_targets():
334+
model = _IdentityAccuracyModel()
335+
data = np.array([[0.1], [0.9]])
336+
targets = np.array([0, 1])
337+
338+
accuracy = model.accuracy(
339+
data,
340+
targets,
341+
batch_size=2,
342+
accuracy_fn=lambda scores: scores[:, 0] > 0.5,
343+
)
344+
345+
assert accuracy == pytest.approx(1.0)
346+
347+
297348
# --- train + evaluate (small integration test) ---
298349

299350

tn4ml/models/model.py

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@
77

88
import funcy
99
import jax
10+
import jax.numpy as jnp
1011
import numpy as np
1112
import optax
1213
import quimb as qu
1314
import quimb.tensor as qtn
14-
from scipy.special import softmax
1515
from tqdm import tqdm
1616

1717
from ..embeddings import *
@@ -21,6 +21,33 @@
2121
logger = logging.getLogger(__name__)
2222

2323

24+
def _as_class_labels(values: jnp.ndarray) -> jnp.ndarray:
25+
"""Convert class scores, one-hot labels, or class labels to label indices.
26+
27+
If values are class scores, the predicted class is selected with argmax.
28+
If values are one-hot labels, the class label is extracted with argmax.
29+
If values are already class labels, they are returned as is.
30+
31+
Parameters
32+
----------
33+
values : jnp.ndarray
34+
Array of class scores, one-hot labels, or class labels.
35+
36+
Returns
37+
-------
38+
jnp.ndarray
39+
Array of class label indices.
40+
"""
41+
values = jnp.asarray(values)
42+
if values.ndim == 0:
43+
return values.reshape((1,)).astype(jnp.int32)
44+
if values.ndim == 1:
45+
return values.astype(jnp.int32)
46+
if values.shape[-1] == 1:
47+
return jnp.squeeze(values, axis=-1).astype(jnp.int32)
48+
return jnp.argmax(values, axis=-1)
49+
50+
2451
def _enable_cpu_multithreading() -> None:
2552
"""Enable XLA multi-threading for CPU backend.
2653
@@ -356,12 +383,13 @@ def forward(
356383

357384
def accuracy(
358385
self,
359-
data: jnp.ndarray,
360-
y_true: jnp.ndarray | None = None,
386+
data: jnp.ndarray | np.ndarray,
387+
y_true: jnp.ndarray | np.ndarray | None = None,
361388
embedding: Embedding | None = None,
362389
batch_size: int = 64,
363390
shuffle: bool = False,
364391
normalize: bool = False,
392+
accuracy_fn: Callable[[jnp.ndarray], jnp.ndarray] | None = None,
365393
dtype: Any = jnp.float_,
366394
seed: int = 42,
367395
alternate_flip: bool = False,
@@ -382,6 +410,10 @@ def accuracy(
382410
Batch size for data processing.
383411
normalize: bool
384412
If True, the model output is normalized in predict function.
413+
accuracy_fn: Callable
414+
Function applied to raw model outputs before class labels are extracted.
415+
If it returns class scores, the predicted class is selected with argmax;
416+
if it returns class labels, those labels are compared directly.
385417
dtype: Any
386418
Data type of input data.
387419
seed: int
@@ -421,14 +453,14 @@ def accuracy(
421453
x = jax.device_put(jnp.array(x, dtype=dtype), _target_device)
422454
y = jax.device_put(jnp.array(y), _target_device)
423455

424-
y_pred = softmax(
425-
jnp.squeeze(_predict_batch(x, embedding, False, normalize)), axis=-1
426-
)
456+
y_pred = _predict_batch(x, embedding, False, normalize)
457+
if accuracy_fn is not None:
458+
y_pred = accuracy_fn(y_pred)
427459

428460
correct_predictions += jnp.sum(
429-
jnp.argmax(y_pred, axis=-1) == jnp.argmax(y, axis=-1)
461+
_as_class_labels(y_pred) == _as_class_labels(y)
430462
)
431-
num_samples += y_pred.shape[0]
463+
num_samples += x.shape[0]
432464

433465
return float(jax.block_until_ready(correct_predictions)) / num_samples
434466

@@ -603,6 +635,7 @@ def train(
603635
val_batch_size: int | None = None,
604636
eval_metric: Callable | None = None,
605637
display_val_acc: bool | None = False,
638+
accuracy_fn: Callable[[jnp.ndarray], jnp.ndarray] | None = None,
606639
dtype: Any = jnp.float_,
607640
shuffle: bool | None = False,
608641
seed: int | None = 42,
@@ -642,6 +675,9 @@ def train(
642675
Number of samples per validation batch.
643676
display_val_acc : bool
644677
If True, displays validation accuracy.
678+
accuracy_fn : Callable
679+
Function applied to raw model outputs before validation accuracy labels
680+
are extracted. Passed to :meth:`accuracy`.
645681
alternate_flip : bool
646682
If True, flips every other batch along axis=1.
647683
@@ -888,6 +924,7 @@ def single_loss(x, y=None):
888924
batch_size=val_batch_size,
889925
embedding=embedding,
890926
shuffle=shuffle,
927+
accuracy_fn=accuracy_fn,
891928
dtype=dtype,
892929
seed=seed,
893930
alternate_flip=alternate_flip,

0 commit comments

Comments
 (0)